美文网首页
Android 保存崩溃日志

Android 保存崩溃日志

作者: 走右边 | 来源:发表于2019-11-28 17:14 被阅读0次

    在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<String, String>()
            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 保存崩溃日志

          本文链接:https://www.haomeiwen.com/subject/uxsmwctx.html