Android 保存崩溃日志

在Android应用上线后,或多或少地都会出现各种问题,尤其是应用崩溃最让人崩溃,如果前期没有做好异常的捕获、崩溃日志的保存和上传的功能,那就很难定位到Bug的位置,久而久之,程序猿的头发又更少了...


要实现崩溃日志工具类,主要是要考虑两个方面的功能:

  • 在出现崩溃时保存错误信息到日志文件
  • 在某一时段上传错误日志(考虑到用户体验,所以放在下次打开应用时自动上传)

然后考虑到保存和上传的时机,大概的流程图应该就是这样:


崩溃日志流程图
1. 保存日志文件

所谓的崩溃都是由于Exception(常见)和Error(不常见)引起的。众所周知:ExceptionError的父类都是Throwable,所以只要在报错的位置捕获到Throwable,然后输出日志到文件即可。

  • 这里有个问题,如何在不知道报错的位置情况下捕获到日志呢?这里就要用到Thread.UncaughtExceptionHandler接口和Thread.setDefaultUncaughtExceptionHandler()方法了。

Thread.UncaughtExceptionHandler的官网解释是:当线程由于未捕获的异常突然终止时调用的处理器的接口。

当线程由于未捕获的异常而即将终止时,Java虚拟机将使用Thread.getUncaughtExceptionHandler()在线程中查询其UncaughtExceptionHandler并将调用处理程序的uncaughtException()方法,将线程和异常作为该方法的参数传递。如果未显式设置线程的UncaughtExceptionHandler,则其ThreadGroup对象将充当其UncaughtExceptionHandler。如果ThreadGroup对象对处理异常没有特殊要求,则可以将调用转发到默认的未捕获异常处理器。
ThreadGroup:顾名思义就是线程所在的线程组,详细可以点击查看。

Thread.setDefaultUncaughtExceptionHandler()官网解释是:设置默认的异常处理器的全局静态方法,传入的必须是Thread.UncaughtExceptionHandler的实现类。

未捕获的异常处理首先由线程控制,然后由线程的ThreadGroup对象控制,最后由默认的未捕获的异常处理器控制。如果线程没有设置显式的未捕获异常处理器,并且线程的线程组(包括父线程组)未专门设置其uncaughtException()方法,则将调用默认处理器的uncaughtException()方法。
通过设置默认的未捕获异常处理器,应用程序可以更改那些已经接受系统提供的“默认”行为的线程的未捕获异常处理方式(例如,记录到特定设备或文件)。
请注意,默认的未捕获异常处理器通常不应遵从线程的ThreadGroup对象,因为这可能导致无限递归。

这样,全局捕获异常的问题算是解决了,接下来新建工具类,实现Thread.UncaughtExceptionHandler接口,这里通过lazy延迟属性,使用双重校验锁实现单例。

class CrashHandler : Thread.UncaughtExceptionHandler {
    companion object {
        //双重校验锁实现单例
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler()
        }
    }

    fun init(context: Context) {
        // 设置CrashHandler为应用的默认异常处理器
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(thread: Thread?, exception: Throwable?) {
          //在此中解析exception
    }
}

这里如何获取Throwable中的信息呢?答案是用StringWriterPrintWriter,在Throwable实例的printStackTrace()方法中获取到堆栈信息。

private fun getExceptionInfo(exception: Throwable?): String {
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        exception?.printStackTrace(pw)
        return sw.toString()
    }

报错日志拿到了,但是不能够去影响到系统处理异常,该报错还是得报错,所以在设置默认异常处理器前要通过Thread.getDefaultUncaughtExceptionHandler()方法获取原来的系统默认处理器,并在保存文件之后,将异常信息原封不动地传给原来的系统默认处理器。

    private var mDefaultCrashHandler: Thread.UncaughtExceptionHandler? = null

    fun init(context: Context) {
        //注意要在设置前获取
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
        // 设置CrashHandler为应用的默认异常处理器
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(thread: Thread?, exception: Throwable?) {
         //在此中解析exception,保存日志文件,可开启子线程写入文件或者使用kotlin的协程
        
        // 系统默认处理
        mDefaultCrashHandler?.uncaughtException(thread, exception)
    }

至此,基本的崩溃日志保存就完成了。什么?怎么保存到文件?直接新建文件夹,写入获取到的堆栈信息到文件,再详细的话欢迎百度。

2. 上传日志文件

上传日志文件这个其实不用多说,用Okhttp或者Retrofit就完事了。
这里主要是考虑上传文件的时机,如果在应用崩溃时保存文件并上传,而且可能等待日志是否上传成功,在这种情况下会导致应用无法操作卡顿后一段时间才崩溃,这样肯定是不行的,所以上传日志文件放在初始化时上传比较好。

3. 日志信息的完善和可自定义

要完善崩溃日志工具类,可能就以下几点:

  • 增加手机基本信息
  • 可控制的日志文件数量
  • 文件存储的位置

获取手机信息然后加入日志文件中,能了解到更多相关信息。日志文件数量可以调节,为0时不保存错误日志。自定义错误日志保存的目录,方便自测时查看。然后,大概就是下面这样子:

/**
 * 崩溃日志处理类
 * @author JPlus
 * @date 2019/3/14.
 */

class CrashHandler : Thread.UncaughtExceptionHandler {
    companion object {
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler()
        }
    }

    private var mDefaultCrashHandler: Thread.UncaughtExceptionHandler? = null
    private var mContext: Context? = null
    private var mDirPath: String? = null
    private var mMaxNum = 0
    /**
     * 初始化
     * @param context 上下文
     * @param maxNum 最大保存文件数量,默认为1
     * @param dir 存储文件的目录,默认为应用私有文件夹下crash目录
     */
    fun init(context: Context, maxNum: Int = 1, dir: String = FileUtils.writePrivateDir("crash", context).absolutePath) {
        mContext = context
        mDirPath = dir
        mMaxNum = maxNum
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    /**
     * 获取最新崩溃日志
     * @return 最新文件
     */
    fun getNewFile(): File? {
        //筛选出最近最新的一次崩溃日志
        return FileUtils.getDirFiles(File(mDirPath))?.let {
            if (it.size>0) it.reversed()[0] else null
        }
    }

    private fun writeNewFile(path: String, name: String, body: String) {
        FileUtils.getDirFiles(File(mDirPath))?.let {
            if (it.size >= mMaxNum) {
                //大于设置的数量则删除最旧文件
                FileUtils.delFileOrDir(it.sorted()[0])
            }
            //继续存崩溃日志,新线程写入文件
            GlobalScope.launch{
                FileUtils.writeFile(File(path, name), body, false)
            }
        }
    }

    /**
     * 当系统中有未被捕获的异常,系统将会自动调用 uncaughtException 方法
     * @param thread
     * @param exception
     */
    override fun uncaughtException(thread: Thread?, exception: Throwable?) {
        val name = AppUtils.instance.getDeviceImei(mContext!!) + "_" + DateUtils.getDateTimeByMillis(false).replace(":", "-")
        val exceptionInfo = StringBuilder(name + "\n\n" + getSysInfo() + "\n\n" + exception?.message)
        exceptionInfo.append("\n" + getExceptionInfo(exception))
        mDirPath?.let {
            if (mMaxNum > 0) {
                writeNewFile(it, "$name.log", exceptionInfo.toString())
            }
        }
        // 系统默认处理
        mDefaultCrashHandler?.uncaughtException(thread, exception)
    }

    private fun getSysInfo(): String {
        val map = hashMapOf()
        map["versionName"] = AppUtils.instance.getAppVersionName(mContext)
        map["versionCode"] = "" + AppUtils.instance.getAppVersionCode(mContext)
        map["androidApi"] = "" + AppUtils.instance.getOsLevel()
        map["product"] = "" + AppUtils.instance.getDeviceProduct()
        map["mobileInfo"] = AppUtils.instance.getDeviceInfo()
        map["cpuABI"] = AppUtils.instance.getCpuABI()
        val str = StringBuilder("=".repeat(10) + "PhoneInfo" + "=".repeat(10) + "\n")
        for (item in map) {
            str.append(item.key).append(" = ").append(item.value).append("\n")
        }
        str.append("=".repeat(10) + "=".repeat(10) + "\n")
        return str.toString()
    }

    private fun getExceptionInfo(exception: Throwable?): String {
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        exception?.printStackTrace(pw)
        return sw.toString()
    }

}

至此,一个简单的崩溃日志工具类实现了,可能或多或少有待改进的地方,欢迎批评指正。

完整项目地址:baselibrary/CrashHandler

你可能感兴趣的:(Android 保存崩溃日志)