Android稳定性:Looper兜底框架实现线上容灾(二)

2,283 阅读2分钟

背景

此前分享过相关内容,见: Android稳定性:可远程配置化的Looper兜底框架

App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。相关数据显示,当Android App的崩溃率超过0.4%的时候,活跃用户有明显下降态势。

项目思路来源于一次提问:有没有办法打造一个永不崩溃的app?

继续深挖这个问题后,我们其实有几个问题需要考虑:

  • 如何打造永不崩溃的 app
  • 当这样做了之后,app 还能正常运行吗?
  • 怎么才能在吃掉异常的同事,让主线程继续运行?
  • 异常被吃掉之后会有什么影响?
  • 到底什么异常需要被吃掉或者说可以吃掉?
  • 吃掉异常能给带来什么好处?是否能对线上问题进行容灾?

这些问题在 Android稳定性:可远程配置化的Looper兜底框架 都被一一解答了。

如何实现、如何更好的实现

实现代码参考 demo 项目: scuzoutao/AndroidCrashProtect

主要的核心代码就两部分:

1. 按配置判断是否需要保护

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                                                                                                            
}                                                                                                                           

2. 实现保护,looper 兜底:

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                                               
        }                                                       
    }                                                           
}                                                               

如何线上容灾

其实思路很简单,问几个问题,答完就知道了。

  1. 崩溃兜底机制可以保护 app,已知我们用配置文件来描述崩溃画像,那配置文件能否远程下发?

  2. 配置文件远程下发后,app 拉下来后能否立即生效?

  3. 假如线上出了个崩溃,崩溃本身涉及代码流程不重要,但是会让 app 直接挂掉,能否线上修改配置文件,将这个崩溃包括进去进行保护,后续在下版本修复之?

崩溃画像实例

[  {    "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": []
  }
]

好了,拜拜。

你可能感兴趣

Android QUIC 实践 - 基于 OKHttp 扩展出 Cronet 拦截器 - 掘金 (juejin.cn)

Android启动优化实践 - 秒开率从17%提升至75% - 掘金 (juejin.cn)

如何科学的进行Android包体积优化 - 掘金 (juejin.cn)

Android稳定性:Looper兜底框架实现线上容灾(二) - 掘金 (juejin.cn)

基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)

记 AndroidStudio Tracer工具导致的编译失败 - 掘金 (juejin.cn)

Android 启动优化案例-WebView非预期初始化排查 - 掘金 (juejin.cn)

chromium-net - 跟随 Cronet 的脚步探索大致流程(1) - 掘金 (juejin.cn)

Android稳定性:可远程配置化的Looper兜底框架 - 掘金 (juejin.cn)

一类有趣的无限缓存OOM现象 - 掘金 (juejin.cn)

Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) - 掘金 (juejin.cn)

Android - 彻底消灭OOM的实战经验分享(千分之1.5 -> 万分之0.2) - 掘金 (juejin.cn)