Android进阶之路 - 错误收集、统计

190 阅读3分钟

错误收集这项功能,应该是我很早以前就想写,但是一直没写的一篇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) {

                    }
                });
    }