背景
此前分享过相关内容,见: Android稳定性:可远程配置化的Looper兜底框架
App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。相关数据显示,当Android App的崩溃率超过0.4%的时候,活跃用户有明显下降态势。
项目思路来源于一次提问:有没有办法打造一个永不崩溃的app?
继续深挖这个问题后,我们其实有几个问题需要考虑:
- 如何打造永不崩溃的 app
- 当这样做了之后,app 还能正常运行吗?
- 怎么才能在吃掉异常的同事,让主线程继续运行?
- 异常被吃掉之后会有什么影响?
- 到底什么异常需要被吃掉或者说可以吃掉?
- 吃掉异常能给带来什么好处?是否能对线上问题进行容灾?
这些问题在 Android稳定性:可远程配置化的Looper兜底框架 都被一一解答了。
如何实现、如何更好的实现
实现代码参考 demo 项目: scuzoutao/AndroidCrashProtect
主要的核心代码就两部分:
- 按配置判断是否需要保护
kotlin
fun needBandage(throwable: Throwable): Boolean {
if (crashPortrayConfig.isNullOrEmpty()) {
return false
}
val config: List<CrashPortray>? = crashPortrayConfig
if (config.isNullOrEmpty()) {
return false
}
for (i in config.indices) {
val crashPortray = config[i]
if (!crashPortray.valid()) {
continue
}
//1. app 版本号
if (crashPortray.appVersion.isNotEmpty()
&& !crashPortray.appVersion.contains(actionImpl.getVersionName(application))
) {
continue
}
//2. os_version
if (crashPortray.osVersion.isNotEmpty()
&& !crashPortray.osVersion.contains(Build.VERSION.SDK_INT)
) {
continue
}
//3. model
if (crashPortray.model.isNotEmpty()
&& crashPortray.model.firstOrNull { Build.MODEL.equals(it, true) } == null
) {
continue
}
val throwableName = throwable.javaClass.simpleName
val message = throwable.message ?: ""
//4. class_name
if (crashPortray.className.isNotEmpty()
&& crashPortray.className != throwableName
) {
continue
}
//5. message
if (crashPortray.message.isNotEmpty() && !message.contains(crashPortray.message)
) {
continue
}
//6. stack
if (crashPortray.stack.isNotEmpty()) {
var match = false
throwable.stackTrace.forEach { element ->
val str = element.toString()
if (crashPortray.stack.find { str.contains(it) } != null) {
match = true
return@forEach
}
}
if (!match) {
continue
}
}
//7. 相应操作
if (crashPortray.clearCache == 1) {
actionImpl.cleanCache(application)
}
if (crashPortray.finishPage == 1) {
actionImpl.finishCurrentPage()
}
if (crashPortray.toast.isNotEmpty()) {
actionImpl.showToast(application, crashPortray.toast)
}
return true
}
return false
}
- 实现保护,looper 兜底:
kotlin
override fun uncaughtException(t: Thread, e: Throwable) {
if (CrashPortrayHelper.needBandage(e)) {
bandage()
return
}
//崩吧
oldHandler?.uncaughtException(t, e)
}
/**
* 让当前线程恢复运行
*/
private fun bandage() {
while (true) {
try {
if (Looper.myLooper() == null) {
Looper.prepare()
}
Looper.loop()
} catch (e: Exception) {
uncaughtException(Thread.currentThread(), e)
break
}
}
}
如何线上容灾
其实思路很简单,问几个问题,答完就知道了。
-
崩溃兜底机制可以保护 app,已知我们用配置文件来描述崩溃画像,那配置文件能否远程下发?
-
配置文件远程下发后,app 拉下来后能否立即生效?
-
假如线上出了个崩溃,崩溃本身涉及代码流程不重要,但是会让 app 直接挂掉,能否线上修改配置文件,将这个崩溃包括进去进行保护,后续在下版本修复之?
崩溃画像实例
css
[ { "class_name": "", "message": "No space left on device", "stack": [],
"app_version": [],
"clear_cache": 1,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "BadTokenException",
"message": "",
"stack": [],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "IllegalStateException",
"message": "not running",
"stack": [
"Daemons"
],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "",
"message": "Activity client record must not be null to execute",
"stack": [],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
},
{
"class_name": "",
"message": "The previous transaction has not been applied or aborted",
"stack": [],
"app_version": [],
"clear_cache": 0,
"finish_page": 0,
"toast": "",
"os_version": [],
"model": []
}
]