错误收集这项功能,应该是我很早以前就想写,但是一直没写的一篇blog,不是因为这项功能不常用,而是因为一直没有接到这样需求,担心随便写误导他人…
年初的时候(时间太无情)写完了错误收集的相关功能,一直没有时间来记录,抽着空闲之余特此记录一波
写错误收集这项功能时,并未借鉴他人blog,而是一路自己撸了出来,经亲测,可用~
在我从业期间,我认为常见的错误收集一般有本地错误收集和三方平台的错误统计
,早之前我记录过一篇三方平台的 Bugly集成 - 异常捕获,有需求可用前去集成(关于类似的平台有很多,自己选择适合自己的即可)
因为我在项目中已经集成过 Bugly异常捕获,为了保险起见,我同时也将错误进行了本地记录
关于本地错误收集,我们的实现思路主要有以下几步
- 如何收集到用户遇到的错误信息?
- 如何记录收集到的错误信息?
- 如何让后台进行错误统计?
如何收集到用户遇到的错误信息?
早期的时候,我有写过一篇 异常捕获崩溃后重新自启动App 的 blog,这里主要借用文内的异常捕获方法(封装版)!
捕获异常
CrashHandler
package nk.com.restartapp;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
/**
* @author MrLiu
* @date 2020/5/13
* desc 捕获异常
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefaultHandler;
MyApplication application;
public CrashHandler(MyApplication myApplication) {
// // 获取系统默认的UncaughtException处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
this.application = myApplication;
}
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false.
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
StackTraceElement[] elements = ex.getStackTrace();
StringBuilder reason = new StringBuilder(ex.toString());
if (elements != null && elements.length > 0) {
for (StackTraceElement element : elements) {
reason.append("\n");
reason.append(element.toString());
}
}
LogTool.e("捕获异常:" + ex);
LogTool.e("捕获异常:" + reason.toString());
LogTool.e("捕获异常:" + ex.getMessage());
LogTool.e("捕获异常:" + getMessage(ex));
LogTool.e("捕获异常:" + ex.toString());
// LookHere : getMessage(ex) 就是我们捕获到的错误,这里我们将错误存于本地
// pushLocal 具体实现位于下方 如何记录收集到的错误信息?
pushLocal(getMessage(ex));
}
public static String getMessage(Throwable e) {
String msg = null;
if (e instanceof UndeclaredThrowableException) {
Throwable targetEx = ((UndeclaredThrowableException) e).getUndeclaredThrowable();
if (targetEx != null) {
msg = targetEx.getMessage();
}
} else {
msg = e.getMessage();
}
return msg;
}
}
基础配置
初始化 - MyApplication
package nk.com.restartapp;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
/**
* @author MrLiu
* @date 2020/5/13
* desc
*/
public class MyApplication extends Application {
private static Context context;
List<Activity> activityList = new ArrayList<>();
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
// 设置该CrashHandler为程序的默认处理器
CrashHandler unCeHandler = new CrashHandler(this);
Thread.setDefaultUncaughtExceptionHandler(unCeHandler);
LogTool.e("配置:Application 异常监听初始配置完成");
}
/**
* context
*/
public static Context getContext() {
return context;
}
/**
* @param activity activity关闭时,删除Activity列表中的Activity对象
*/
public void removeActivity(Activity activity) {
activityList.remove(activity);
}
/**
* @param activity 向列表中添加Activity对象
*/
public void addActivity(Activity activity) {
activityList.add(activity);
}
/**
* 关闭Activity列表中的 所有Activity
*/
public void finishAllActivity() {
for (Activity activity : activityList) {
if (null != activity) {
activity.finish();
}
}
// 杀死应用进程
android.os.Process.killProcess(android.os.Process.myPid());
}
}
清单注册application - 注册我们自己的MyApplication
android:name=".MyApplication"
示例如下:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nk.com.restartapp">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
如何记录收集到的错误信息?
关于错误收集的方式,我使用过以下俩种方式
其实我也有考虑过 文件存储 ,但是因为不是键值对的存储形式,故不太利于后台入库,也会影响后续功能扩展,局限性太大,
SP存储
我最初采用的是 SP存储 ,但是考虑到数据安全性与存储量后,还是抛弃了此种选型;最后还是在此处做一下记录吧,毕竟实现非常简单
pushLocal - 数据记录(关于内部的sp方法,可以前往此处查看 SP存储 )
public static void pushLocal(int type, String errorMsg, String errorInfo) {
//临时集合,用于存储错误数据
List<ErrorBean> tmpList = new ArrayList<>();
//判断是否已有错误数据,如有则移到临时集合中
List<ErrorBean> errorList = SpUtil.getnewListData("errorData", ErrorBean.class);
if (errorList != null) {
if (errorList.size() > 0) {
tmpList.clear();
for (int i = 0; i < errorList.size(); i++) {
tmpList.add(errorList.get(i));
}
errorList.clear();
}
}
//存入刚收集的错误数据
ErrorBean errorBean = new ErrorBean();
errorBean.setType(type);
errorBean.setOccurrenceTime(Long.parseLong(DateTool.getCurrentTimeMillis()));
errorBean.setErrorInfo(errorInfo);
errorBean.setErrorMsg(errorMsg);
errorBean.setMachineId(ToolUtil.isStringNull(SpUtil.getString("machine_Id")) ? "" : SpUtil.getString("machine_Id"));
tmpList.add(errorBean);
SpUtil.putnewListData("errorData", tmpList);
}
数据库存储
关于数据库选型,我使用的是之前一直在用的GreenDao数据库,关于 GreenDao全面讲解 都在我的另一篇blog中
pushLocal - 数据记录(关于存储的数据参数,根据业务需求自行设计)
public static void pushLocal(int type, String errorMsg, String errorInfo) {
LogTool.e("time:" + DateTool.getStrTime(DateTool.getCurrentTimeMillis()));
ErrorEntity errorBean = new ErrorEntity();
errorBean.setType(type);
errorBean.setOccurrenceTime(Long.parseLong(DateTool.getCurrentTimeMillis()));
errorBean.setErrorInfo(errorInfo);
errorBean.setErrorMsg(errorMsg);
errorBean.setMachineId(ToolUtil.isStringNull(SpUtil.getString("machine_Id")) ? "" : SpUtil.getString("machine_Id"));
GreenBasic.getInstance().getSession().getErrorEntityDao().insert(errorBean);
}
如何让后台进行错误统计?
移动端已经完成错误收集,也已经存储在了本地,剩余最后一步就是我们将本地存储的错误数据传到后台了,之后我们就可以通过后台查看错误,然后优化我们的程序了
关于何时上传错误日志,每个人可能有不同的看法,以下是我能想到的
- 定时上传,移动端写个定时器,规定一定的时间自行上传日志在后台(及时性一般)
- 被动上传,建立长连接,当后台发信息后,我们再将错误日志进行上传(及时性一般)
- 重启上传,每次app重启后,在
MainonCreate()周期内主动上传错误日志
(我目前使用的这一种方式,个人感觉实现方便,有效性也还行)
上传本地错误日志
这里是读取存储于数据库的错误数据后转为json,然后通过接口上传即可(传sp数据的话,就前俩行获取数据方式不同,其实都一样),我的网络框架用的Retrofit + Rx(一直没抽出时间来整理,改天补上…),json转换使用的是已经用了很多年的 FastJson ~
Here:错误数据上传成功后可以删除本地数据,也可以通过一个type值做标记,之后每次传日志的时候通过type过滤 已传/未传 数据,这样做好处是留有业务扩展控件,坏处就是占用-用户内存 ~
private void uploadLog() {
ErrorEntityDao errorBeanDao = GreenBasic.getInstance().getSession().getErrorEntityDao();
List<ErrorEntity> errorList = (List<ErrorEntity>) DaoStrategy.getInstance(new LyError()).queryConditionList(false);
if (errorList == null || errorList.size() <= 0) {
return;
}
String pushJson = JSON.toJSONString(errorList);
HashMap<String, Object> map = new HashMap<>();
map.put("errMsg", pushJson);
RetrofitManager.getInstance()
.getApiService()
.uploadErrMsg(map)
.compose(RxHelper.observableIO2Main(this))
.subscribe(new BaseObserver() {
@Override
public void onSuccess(Object response) {
for (int i = 0; i < errorList.size(); i++) {
ErrorEntity bean = errorList.get(i);
errorBeanDao.update(new ErrorEntity(bean.getId(), bean.getOccurrenceTime(), bean.getMachineId(), bean.getType(), bean.getErrorInfo(), bean.getErrorMsg(), true));
}
errorList.clear();
}
@Override
public void onFailure(Throwable e, String errorMsg) {
}
});
}