概述

在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? = nullprivate var defaultHandler: UncaughtExceptionHandler? = nullprivate val info = HashMap<String, String>()init {init(context)}private fun init(context: Context) {this.context = contextdefaultHandler = 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: Stringfor (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.causewhile (cause != null) {cause.printStackTrace(printWriter)cause = cause.cause}printWriter.close()val result = writer.toString()sb.append(result)// 保存到文件var fos: FileOutputStream? = nullval 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!!.packageManagerval pi = pm.getPackageInfo(context.packageName, PackageManager.GET_ACTIVITIES)if (pi != null) {val versionName = if (pi.versionName == null) "null" else pi.versionNameval versionCode = pi.versionCode.toString() + ""info.put("versionName", versionName)info.put("versionCode", versionCode)}} catch (e: NameNotFoundException) {e.printStackTrace()}val fields = Build::class.java.declaredFieldstry {for (field in fields) {field.isAccessible = trueinfo.put(field.name, field.get(null).toString())}} catch (e: Exception) {e.printStackTrace()}}companion object {val TAG = "CrashExceptionHandler"private var instance: CrashExceptionHandler? = nullfun 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.pathtry {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.9compile '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 Crash日志收集相关推荐

  1. 代码:android崩溃日志收集和处理

    用来处理android崩溃日志收集的代码,详情的使用请转:android崩溃日志收集和处理 第一个类 /** * 异常捕捉实现类 */ public class ErrorCaughtimplemen ...

  2. Android平台日志收集系统

    Android平台日志收集系统 在产品开发测试中以及产品投放到终端客户后,我们经常会遇到各种各样的问题,产品出异常,比较严重的就是使用过程中死机,用户无法操作.对于这种情况,将问题反馈给研发,问题能够 ...

  3. android崩溃日志收集

    https://blog.csdn.net/wxx_csdn/article/details/79238842 转载于:https://blog.51cto.com/xuguohongai/21289 ...

  4. ios崩溃日志收集_漫谈iOS Crash收集框架

    为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如 KSCrash,plcrashreporter,CrashKit 等.追求方便省心,对于保密性要求不高的程 ...

  5. android crash没有日志_Android开发必备神器CrashCanary

    阅读本文大概需要8分钟 作者:wangsj1992出处:https://www.jianshu.com/p/8676f7a05920 前言 安卓开发中,你是否遇到过如下困扰: 场景一 开发好一个功能后 ...

  6. app运行中的crash崩溃异常日志收集

    转载来源http://blog.csdn.net/qq_17387361/article/details/52688998 在Android开发中,一个app在推广后.我们怎么才能知道这个app运行的 ...

  7. android crash没有日志_App测试之monkey(四)-调试参数及日志

    由于monkey在测试app时,我们需要作长时间的稳定性测试,比如连续测试10小时(monkey不能指定时间,可以指定次数,时间可以在测试次数的日志基础上大概算出来),在测试过程中,app很可能测试时 ...

  8. Android studio中NDK开发(四)——使用addr2line分析Crash日志 backtrace

    文章目录 一.前言 二.分析 1.先提取backtrace部分 2.提取对应so库的信息 3.提取错误地址 三.使用addr2line对地址进行转换 1.addr2line工具的路径放在 2.Term ...

  9. android日志收集存入mysql_rsyslog+analyzer+mysql实现日志收集展示

    why->what->where->when->who->how 1.为什么要进行日志收集?为什么要用到rsyslog? 日志是我们对系统和应用程序的运行状况分析的根本依 ...

最新文章

  1. 显卡不够时,如何训练大型网络
  2. 整合rpc远程调用_远程过程调用(RPC)
  3. Python中制作词云的WordCloud参数详解
  4. 冲刺阶段——Day5
  5. [ASP.NET MVC2 系列] ASP.NET MVC 之如何创建自定义路由约束
  6. SQL Server 用表中已有数据造数据
  7. tiantianguandan官方网站
  8. Listen第二个参数的意义
  9. linux打印JAVA日志命令_Linux下查看日志用到的常用命令
  10. 【java笔记】scanner类和匿名对象的使用
  11. 时间操作(struct tm、time_t)求指定日期 前n天的日期
  12. 精通Hyperledger之Hyperledger composer建模语言(15)
  13. 计算机进入端口模式命令提示符,Win10使用命令提示符删除端口占用方法Win10查看端口占用状态...
  14. 怎样发表期刊才能快速通过
  15. 【小林课堂】【光学】透镜成像应用
  16. 微信小程序组件开发——可视化电影选座
  17. Python的数据类型
  18. 微信小程序开发笔记(二)
  19. 长沙理工大学计算机电路b试题,长沙理工大学考试试卷(计算机网络)
  20. 结算时打印购物小票,计算此次获得的会员积分

热门文章

  1. UEFI启动模式下win10+Ubuntu18.04双硬盘(固态+机械)双系统安装2019.06船新版本
  2. 表现层、持久层、业务层
  3. 高德地图车机版(修改共存后去除启动弹窗弹窗代码)
  4. 删除右键菜单中的Git
  5. 经典电影台词.....
  6. PHP将某个页面导出为pdf文件
  7. 解锁优麒麟的隐藏快捷键玩法,效率UPUP
  8. MapReduce编程
  9. HTML如何编写类似QQ聊天框,jQuery实现简易QQ聊天框
  10. ORACLE · 保留两位小数的三种方法 · 方法一 round()函数