社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
在Android应用的开发过程中,总会遇到应用程序Crash。在编码阶段,设备连接到PC,可以在Android Studio的Logcat中可以查看Crash的信息。但是很明显,靠这种方式收集Crash日志修改bug,实在是太不靠谱,一旦APP发布测试甚至生产环境,如果没有一个Crash日志的反馈,那么将会是一个噩梦,所以本文的目的:
有了Bugly或者其它第三方平台(如友盟等),可以很方便地管理Crash日志,通过分析日志可以尽快解决bug。而写入设备SD卡中的Crash日志文件则方便在测试阶段查看和分析,必要时也可以发送到自己的后台服务器。
应用程序的Crash事件是可以捕抓的,在自定义的Application中添加一行代码,UncaughtExceptionHandler接口即可:
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(CrashExceptionHandler.getInstance(this))
}
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主要有几个原因,第一,接入简单快捷;第二,每一个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
如果APP在Google Play上发布,那么Google Play会统计Crash信息。打开Google Play Console,选择你的应用,在Android Vitals菜单的下级菜单中有“ANR和崩溃次数“选项,点击进去即可查看Crash统计。
一般来说,有了bugly帮我们统计Crash,我们要做的就是上传符号表,然后分析定位Crash日志即可,但是为了更深入地了解Crash日志收集和分析,有必要做更深入的了解。
通常情况下收集Crash的堆栈信息已经足够我们分析并定位出崩溃的原因,从而修复这个Crash。但是复杂一点的Crash,可能靠仅有堆栈信息是不够的,我们还需要其它一些信息来辅助问题的定位和解决,这些信息包括如下内容:线程信息,SharedPreference信息,系统设置,Locat中的日志MenInfo,自定义Log文件日志。——《Android高级进阶》
Java层的Crash捕抓由于有Java提供了接口,所以捕获和分析相对来说比较简单,而如果Crash发生在Native层,那么Crash日志捕抓和分析将会变得复杂。大多数时候看到Native抛出的Crash日志,会感到束手无策。如果项目中有Native项目,那么就有必要研究如何处理Native层Crash日志的捕获和分析,这对问题的定位和解决有很大帮助。
有了文件日志,Bugly上报及管理,Crash日志收集基本没有遗漏的了。如果应用发布到Google Play,可以好好利用Google Play Console上的Crash统计信息。Crash有时候感觉是毫无道理的,Bug是改不完的,要做一个好的APP,要好的用户体验,我们能做的就是及时发现问题,不断修改,这些工具可以很好地为我们服务,而更重要的是我们的态度:及时发现,及时解决!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!