Android Crash日志收集

本文介绍了在Android开发中如何实现自定义的UncaughtExceptionHandler来收集Crash信息,并将其存储在SD卡中。同时,文章详细讲解了接入Bugly第三方服务进行Crash日志的上报与管理,以及如何在Google Play Console中查看Crash统计信息。此外,还探讨了扩展Crash收集以捕获更多信息和处理Native层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,要好的用户体验,我们能做的就是及时发现问题,不断修改,这些工具可以很好地为我们服务,而更重要的是我们的态度:及时发现,及时解决!

### 查看和解析 Android 应用崩溃时生成的日志信息 #### 1. 获取 Crash 日志的方法 Android 应用崩溃时,系统会自动生成崩溃日志。这些日志包含了崩溃时刻的堆栈跟踪、异常类型和其他上下文信息[^1]。以下是几种常用的获取方法: - **Logcat 输出** Logcat 是 Android 开发中最常用的调试工具之一。通过连接设备或运行模拟器,在 Android Studio 中打开 Logcat 面板,并设置合适的过滤条件(如 `tag:CRASH`),即可捕获崩溃日志[^2]。 - **adb logcat 命令** 可以使用 adb 工具直接从终端抓取日志: ```bash adb logcat | grep "FATAL EXCEPTION" ``` 这条命令会筛选出包含致命错误的信息,便于快速定位问题。 - ** Tombstone 文件** 当应用程序发生 Native 层崩溃时,可能会生成 tombstone 文件。这类文件存储在 `/data/tombstones/` 路径下,需具备 root 权限才能访问。如果无法获得 root 权限,可尝试通过以下方式导出: ```bash adb shell ls /data/tombstones/ adb pull /data/tombstones/<tombstone_file> ./ ``` #### 2. 分析 Crash 日志的内容 崩溃日志通常由以下几个部分组成: - **异常类型** 描述了导致崩溃的具体 Java 异常类名,例如 `NullPointerException`, `ArrayIndexOutOfBoundsException` 等[^1]。 - **堆栈跟踪 (Stack Trace)** 记录了引发崩溃的操作序列及其所在的源代码位置。每一行都指明了一个函数调用的位置以及参数传递的情况。 - **线程信息** 显示当前正在执行的任务所属的线程名称及 ID。对于多线程环境尤为重要,因为某些类型的 Bug 只会在特定条件下暴露出来。 - **内存转储 (Memory Dump)** 在 Native Crash 情况下尤为有用,提供了更多底层硬件层面的状态快照[^3]。 #### 3. 使用工具辅助分析 为了提高效率,可以借助专门设计用于处理复杂场景的外部工具和服务: - **Google Breakpad** Google 提供了一款名为 Breakpad 的开源解决方案,适用于监控非系统级应用发生的 native crashes。它可以捕捉到完整的 minidump 数据包,方便后续离线诊断[^3]。 - **Firebase Crashlytics** Firebase Crashlytics 是一款强大的在线服务,支持实时收集来自生产环境中用户的反馈报告。不仅限于纯文本形式展现,还集成了图表统计功能,有助于发现趋势模式[^4]。 ```python import os os.system('adb logcat -d > crash_log.txt') # 将最近的日志保存为本地文件 ``` --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值