Android Crash日志收集 - Go语言中文社区

Android Crash日志收集


概述

在Android应用的开发过程中,总会遇到应用程序Crash。在编码阶段,设备连接到PC,可以在Android Studio的Logcat中可以查看Crash的信息。但是很明显,靠这种方式收集Crash日志修改bug,实在是太不靠谱,一旦APP发布测试甚至生产环境,如果没有一个Crash日志的反馈,那么将会是一个噩梦,所以本文的目的:

  1. 实现自定义的UncaughtExceptionHandler,收集Crash的信息和设备的基本信息,并写入设备的SD卡中;
  2. 接入第三方SDK(Bugly),收集和上报到第三方平台;
  3. 发布到Google Play的APP,在Google Play Console中查看Crash统计信息。

有了Bugly或者其它第三方平台(如友盟等),可以很方便地管理Crash日志,通过分析日志可以尽快解决bug。而写入设备SD卡中的Crash日志文件则方便在测试阶段查看和分析,必要时也可以发送到自己的后台服务器。

自定义 UncaughtExceptionHandler

捕抓Crash事件

应用程序的Crash事件是可以捕抓的,在自定义的Application中添加一行代码,UncaughtExceptionHandler接口即可:

override fun onCreate() {
    super.onCreate()
    Thread.setDefaultUncaughtExceptionHandler(CrashExceptionHandler.getInstance(this))
}

实现CrashExceptionHandler

UncaughtExceptionHandler接口只有一个方法uncaughtException(thread: Thread, ex: Throwable),Crash信息就在ex中。除了Crash日志外,为了方便调试,一般还需要收集设备的基本信息(在collectDeviceInfo(context)中处理),然后把收集到的信息写入文件(writeCrashInfoIntoFile(ex)),最后将Crash交给默认的UncaughtExceptionHandler处理,基本就完成了:

class CrashExceptionHandler private constructor(context: Context) : UncaughtExceptionHandler {
    override fun uncaughtException(thread: Thread, ex: Throwable) {
        collectDeviceInfo(context)
        writeCrashInfoIntoFile(ex)
        defaultHandler!!.uncaughtException(thread, ex)
    }
}

CrashExceptionHandler类的完整代码如下:


import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.util.Log
import java.io.*
import java.lang.Thread.UncaughtExceptionHandler
import java.text.SimpleDateFormat
import java.util.*

class CrashExceptionHandler private constructor(context: Context) : UncaughtExceptionHandler {

    private var context: Context? = null
    private var defaultHandler: UncaughtExceptionHandler? = null
    private val info = HashMap<String, String>()

    init {
        init(context)
    }

    private fun init(context: Context) {
        this.context = context
        defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
    }

    override fun uncaughtException(thread: Thread, ex: Throwable) {
        collectDeviceInfo(context)
        writeCrashInfoIntoFile(ex)
        defaultHandler!!.uncaughtException(thread, ex)
    }

    private fun writeCrashInfoIntoFile(ex: Throwable?) {
        if (ex == null) {
            return
        }
        // 设备信息
        val sb = StringBuilder()
        var value: String
        for (key in info.keys) {
            value = info[key]!!
            sb.append(key).append("=").append(value).append("n")
        }
        // 错误信息
        val writer = StringWriter()
        val printWriter = PrintWriter(writer)
        ex.printStackTrace(printWriter)
        var cause: Throwable? = ex.cause
        while (cause != null) {
            cause.printStackTrace(printWriter)
            cause = cause.cause
        }
        printWriter.close()
        val result = writer.toString()
        sb.append(result)
        // 保存到文件
        var fos: FileOutputStream? = null
        val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
        val timestamp = System.currentTimeMillis()
        val time = formatter.format(Date())
        val fileName = "$time-$timestamp.txt"
        try {
            val file = ExternalStorageUtils.getDiskCacheDir(context!!, "crash")
            if (!file.exists()) {
                file.mkdirs()
            }
            val newFile = File(file.absolutePath + File.separator + fileName)
            fos = FileOutputStream(newFile)
            fos.write(sb.toString().toByteArray())
        } catch (fne: FileNotFoundException) {
            Log.e(TAG, fne.message)
        } catch (e: Exception) {
            Log.e(TAG, e.message)
        } finally {
            if (fos != null) {
                try {
                    fos.close()
                } catch (e: IOException) {
                    Log.e(TAG, e.message)
                }

            }
        }
    }

    private fun collectDeviceInfo(context: Context?) {
        try {
            val pm = context!!.packageManager
            val pi = pm.getPackageInfo(context.packageName, PackageManager.GET_ACTIVITIES)
            if (pi != null) {
                val versionName = if (pi.versionName == null) "null" else pi.versionName
                val versionCode = pi.versionCode.toString() + ""
                info.put("versionName", versionName)
                info.put("versionCode", versionCode)
            }
        } catch (e: NameNotFoundException) {
            e.printStackTrace()
        }

        val fields = Build::class.java.declaredFields
        try {
            for (field in fields) {
                field.isAccessible = true
                info.put(field.name, field.get(null).toString())
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

    companion object {

        val TAG = "CrashExceptionHandler"
        private var instance: CrashExceptionHandler? = null

        fun getInstance(context: Context): CrashExceptionHandler {
            if (instance == null) {
                instance = CrashExceptionHandler(context)
            }
            return instance!!
        }
    }

}

代码中的ExternalStorageUtils.getDiskCacheDir(context!!, “crash”)方法:

fun getDiskCacheDir(context: Context, uniqueName: String): File {
    var cachePath = context.cacheDir.path
    try {
        if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() || !ExternalStorageUtils.isExternalStorageRemovable) {
            cachePath = ExternalStorageUtils.getExternalCacheDir(context)!!.path
        }
    } catch (e: Exception) {
        e.printStackTrace()
        Log.e(ExternalStorageUtils.TAG, e.message)
    }

    return File(cachePath + File.separator + uniqueName)
}

接入Bugly

Bugly是腾讯旗下的一个移动端异常上报和运营统计平台,选择Bugly主要有几个原因,第一,接入简单快捷;第二,每一个Crash都有相应的帮助;第三,每天早上都可以收到Crash日报,用户奔溃率,影响用户数,发生次数,联网用户数。

自动集成

详情请参考Bugly Android SDK 使用指南,推荐自动集成。
Bugly SDK分为两部分:SDK和NDK(需要同时集成Bugly SDK),按需添加。自动集成只需要在module中添加相应的依赖即可:

android {
    defaultConfig {
        ndk {
            // 设置支持的SO库架构
            abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
        }
    }
}

dependencies {
    compile 'com.tencent.bugly:crashreport:latest.release' //其中latest.release指代最新Bugly SDK版本号,也可以指定明确的版本号,例如2.1.9
    compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新Bugly NDK版本号,也可以指定明确的版本号,例如3.0
}

配置权限

在AndroidManifest.xml中添加权限:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />

混淆配置

-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}

初始化

在Application类的onCreate方法中添加一行代码:

CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false); 

配置符号表

一般APK都会混淆代码,混淆以后的错误日志类,方法名,变量名变成了a,b,c,d…,所以,为了还原错误日志,需要上传符号表。
1. 自动配置参考Bugly符号表插件使用指南
2. 手动配置参考Bugly Android 符号表配置

notice: 每次发布的时候一定要备份好Debug SO文件和mapping.txt

Google Play Console查看Crash统计信息

如果APP在Google Play上发布,那么Google Play会统计Crash信息。打开Google Play Console,选择你的应用,在Android Vitals菜单的下级菜单中有“ANR和崩溃次数“选项,点击进去即可查看Crash统计。
这里写图片描述

扩展

一般来说,有了bugly帮我们统计Crash,我们要做的就是上传符号表,然后分析定位Crash日志即可,但是为了更深入地了解Crash日志收集和分析,有必要做更深入的了解。

捕抓Crash时收集更多的信息

通常情况下收集Crash的堆栈信息已经足够我们分析并定位出崩溃的原因,从而修复这个Crash。但是复杂一点的Crash,可能靠仅有堆栈信息是不够的,我们还需要其它一些信息来辅助问题的定位和解决,这些信息包括如下内容:线程信息,SharedPreference信息,系统设置,Locat中的日志MenInfo,自定义Log文件日志。——《Android高级进阶》

  1. 线程信息
  2. SharedPreference信息
  3. 系统设置信息
  4. MenInfo信息

Native层Crash捕获机制

Java层的Crash捕抓由于有Java提供了接口,所以捕获和分析相对来说比较简单,而如果Crash发生在Native层,那么Crash日志捕抓和分析将会变得复杂。大多数时候看到Native抛出的Crash日志,会感到束手无策。如果项目中有Native项目,那么就有必要研究如何处理Native层Crash日志的捕获和分析,这对问题的定位和解决有很大帮助。

总结

有了文件日志,Bugly上报及管理,Crash日志收集基本没有遗漏的了。如果应用发布到Google Play,可以好好利用Google Play Console上的Crash统计信息。Crash有时候感觉是毫无道理的,Bug是改不完的,要做一个好的APP,要好的用户体验,我们能做的就是及时发现问题,不断修改,这些工具可以很好地为我们服务,而更重要的是我们的态度:及时发现,及时解决!

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/half_bottle/article/details/78057114
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 20:02:03
  • 阅读 ( 2318 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢