【Android】利用 ACRA 实现在规定时间内崩溃次数超过规定值就自动清理 APP 数据

时间:2020-12-05 07:57:37

其实 ACRA 是一个崩溃处理的类库,其功能就是收集App崩溃堆栈信息,生成报告并发送到指定端,当然它也可以自己定制对应的操作,所以是个处理崩溃很不错的库。

ACRA Application Crash Reports for Android

GitHub:https://github.com/ACRA/acra


其实在规定时间内崩溃次数超过规定值就自动清理 APP 数据这个功能在一些大型APP上都会有,因为这对于解决某些因本地数据导致的崩溃有很好的作用,用户没必要再进行卸载重装,算是一个细节加分。

本文实现的是清理 APP 自身 data 目录下的数据以及 SharedPreferences 里面的数据,如需清理其它目录下的数据请参考代码进行修改。

下面就说说怎么去实现:

首先我们先在 build.gradle 里面添加依赖:

dependencies {

 ...

compile 'ch.acra:acra:4.7.0'

...
}

这里要声明一下为什么要用4.7.0,写这篇文章的时候最新的版本是4.9.2,其实几个版本都试过,4.7.0最合我心意,因为这个版本在 Application 初始化数据崩溃也会被收集,4.7.0之后就不行,所以为了多一层保障而选择了4.7.0。

接下来是自定义 Application:

@ReportsCrashes(
//一些ACRA的设置,具体参考ACRA文档,因为我们使用自定义Sender,所以这里完全可以不用设置
// mailTo = "bugs@treeholeapp.cn",
// mode = ReportingInteractionMode.TOAST,
// resToastText = R.string.crash_toast_text
)
public class MyApplication extends Application {
private static Context context;

@Override
public void onCreate() {
initACRA();
super.onCreate();

context = getApplicationContext();
}

public void initACRA() {
if (!BuildConfig.DEBUG) { //这里判断只有在非DEBUG下才清除数据,主要是为了在开发过程中能够保留线程。
ACRA.init(this);

CrashHandler handler = new CrashHandler();
ACRA.getErrorReporter().setReportSender(handler); //在闪退时检查是否要清空数据
}
}

public static Context getContext(){
return context;
}

}

然后在 AndroidManifest.xml 里面设置 Application:

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="...">
<application
...
android:name="xxx.MyApplication">
...
</application>
</manifest>

接下来就是自定义 ReportSender 类的代码:

public class CrashHandler implements ReportSender {
@Override
public void send(Context context, CrashReportData errorContent) throws ReportSenderException {
//闪退,检查是否需要清空数据
new CrashModel().checkAndClearData();
}
}

然后是崩溃处理 CrashModel 类的代码:

public class CrashModel {
private static final String CRASH_TIME_FILE_NAME = "crash_time";
//不能通过App.getPackageName来获取包名,否则会有问题,只能默认为cn.campusapp.campus。所以对于debug或者运营版本,清数据会把release的清掉
private static final String FILE_DIR = String.format("/data/data/%s/", BuildConfig.APPLICATION_ID);

protected ArrayList<Long> mCrashTimes;
Gson gson = new Gson();

public CrashModel() {
mCrashTimes = readCrashTimes();
if (mCrashTimes == null) {
mCrashTimes = new ArrayList<>();
storeCrashTimes(mCrashTimes);
}
}


public void checkAndClearData() {
long timeNow = System.currentTimeMillis();

if (checkClearData(timeNow, new ArrayList<>(mCrashTimes))) {
//已经在5分钟之内有三次闪退,需要清理数据
try {
clearData();
}
catch (Exception e) {
//清空所有数据失败
}
}
else {
mCrashTimes.add(timeNow);
storeCrashTimes(mCrashTimes);
//此次不需要清空数据, 崩溃此时:gson.toJson(mCrashTimes));
}
}

private void storeCrashTimes(ArrayList<Long> crashTimes) {
try {
String str = gson.toJson(crashTimes);
FileUtil.writeToFile(FILE_DIR + CRASH_TIME_FILE_NAME, str);
}
catch (Exception e) {
//保存闪退时间失败
}

}

private ArrayList<Long> readCrashTimes() {
try {
String timeStr = FileUtil.readFileContent(FILE_DIR + CRASH_TIME_FILE_NAME);
return gson.fromJson(timeStr, new TypeToken<ArrayList<Long>>() {
}.getType());
}
catch (Exception e) {
//读取闪退时间失败
}
return null;
}

/**
* 检查是否需要清空数据,目前的清空策略是在5分钟之内有三次闪退的就清空数据,也就是从后往前遍历,只要前两次闪退发生在5分钟之内,就清空数据
*
* @return
*/
private boolean checkClearData(long time, ArrayList<Long> crashTimes) {
//Timber.i(gson.toJson(crashTimes));
int count = 0;
for (int i = crashTimes.size() - 1; i >= 0; i--) {
long crashTime = crashTimes.get(i);
if (time - crashTime <= 5 * 60 * 1000) {
count++;
if (count >= 2) {
break;
}
}
}
if (count >= 2) {
//在5分钟之内有三次闪退,这时候需要清空数据
return true;
} else {
return false;
}
}

/**
* 清空数据,包括数据库中的和SharedPreferences中的
*
* @throws Exception
*/
private void clearData() throws Exception {
//开始清理数据
FileUtil.delFolderNew(FILE_DIR);
SharedPreUtil.getInstance().Clear();
}
}

这里需要用到 Gson ,请从 GitHub 获取 jar:

https://github.com/google/gson


然后还有一个文件处理类 FileUtil:

public class FileUtil {
/**
* Prints some data to a file using a BufferedWriter
*/
public static boolean writeToFile(String filename, String data) {
BufferedWriter bufferedWriter = null;
try {
// Construct the BufferedWriter object
bufferedWriter = new BufferedWriter(new FileWriter(filename));
// Start writing to the output stream
bufferedWriter.write(data);
return true;
} catch (FileNotFoundException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
} finally {
// Close the BufferedWriter
try {
if (bufferedWriter != null) {
bufferedWriter.flush();
bufferedWriter.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return false;
}

/**
* 读文件,并返回String
* @param strFileName 文件名
*/
public static String readFileContent(String strFileName) throws IOException {
File file = new File(strFileName);
return readFileContentAsString(file, null);
}


public static String readFileContentAsString(File file, String charsetName) throws IOException {
if (!file.exists() || !file.isFile()) {
throw new IOException("File to be readed not exist, file path : " + file.getAbsolutePath());
}

FileInputStream fileIn = null;
InputStreamReader inReader = null;
BufferedReader bReader = null;
try {
fileIn = new FileInputStream(file);
inReader = charsetName == null ? new InputStreamReader(fileIn) : new InputStreamReader(fileIn, charsetName);
bReader = new BufferedReader(inReader);
StringBuffer content = new StringBuffer();
char[] chBuffer = new char[1024];
int readedNum = -1;
while ((readedNum = bReader.read(chBuffer)) != -1) {
content.append(chBuffer, 0, readedNum);
}

return content.toString();
} finally {
if (fileIn != null) {
try {
fileIn.close();
} catch (IOException e) {
}
}

if (bReader != null) {
try {
bReader.close();
} catch (IOException e) {
}
}
}
}


/**
* 删除文件夹(调整后的)
*
* @param folderPath
* String 文件夹路径及名称 如c:/fqf
*/
public static void delFolderNew(String folderPath) {
try {
delAllFileNew(folderPath); // 删除完里面所有内容

File myFilePath = new File(folderPath);
myFilePath.delete(); // 删除空文件夹

}
catch (Exception e) {
System.out.println("删除文件夹操作出错");
e.printStackTrace();
}
}

/**
* 删除文件夹里面的所有文件(调整后的)
* @param path String 文件夹路径 如 c:/fqf
*/
public static void delAllFileNew(String path) {
File file = new File(path);
if (!file.exists()) {
return;
}
if (!file.isDirectory()) {
return;
}

String[] tempList = file.list();
File temp = null;
String tempPath = "";

for (int i = 0; i < tempList.length; i++) {
if (path.endsWith(File.separator)) {
temp = new File(path + tempList[i]);
tempPath = path + tempList[i];
}
else {
temp = new File(path + File.separator + tempList[i]);
tempPath = path + File.separator + tempList[i];
}
if (temp.isFile()) {
temp.delete();
}
if (temp.isDirectory()) {
delFolderNew(tempPath);// 先删除文件夹里面的文件
}
}
}
}
(文件处理工具类很实用,这里只展示了要用到的一部分,可以自己再丰富其方法。)


最后还有一个处理 SharedPreferences 数据的工具类:

public class SharedPreUtil{
    private SharedPreUtil() {}
    private static SharedPreUtil sharedpreutil;

    public static SharedPreUtil getInstance() {
        if (sharedpreutil == null) {
            synchronized (SharedPreUtil.class) {
                sharedpreutil = new SharedPreUtil();
            }
        }
        return sharedpreutil;
    }

    public static void clear(){
        SharedPreferences prefer = MyApplication.getContext().getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE);
        SharedPreferences.Editor editor = prefers.edit();

        editor.remove("key1");
        editor.remove("key2");
        editor.remove("key3");
    }
}

其思路就是通过 MyApplication 获得 context,然后获取 SharedPreferences,再进行数据处理。


这里只是对崩溃次数进行判断以及本地数据清理处理,如果想进行上报崩溃记录等操作,可以再自行研究下 ACRA 这个库。