Android稳定性:可远程配置化的Looper兜底框架
邹阿涛涛涛涛涛涛 人气:0正文
App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。相关数据显示,当Android App的崩溃率超过0.4%的时候,活跃用户有明显下降态势。
目前受益于我司采取的一系列的治理、监控、防劣化体系,java crash率降低到了一个十万分级别的数字**,**今天分享的就是稳定性治理过程中的一个重要工具,下面开整。
1. 为什么抛出异常时app会退出
不细致分析了,网上随便找一下就是一堆博客,简单来说就是没有被catch的崩溃抛出时,会调用 Thread#dispatchUncaughtException(throwable)
来进行处理,而在进程初始化时,RuntimeInit#commonInit
里会注入默认杀进程的 KillApplicationHandler
,如果我们没有实现自定义的 UncaughtExceptionHandler
时,dispatchUncaughtException
被调用就会走到 KillApplicationHandler
里,把当前的进程杀掉,即产生了一次用户感知的崩溃行为。
2. 有没有办法打造一个永不崩溃的app
这个问题问出来的前提是指发生的崩溃是来自于 java 层面的未捕获的异常,c 层就是另一回事了。我们来尝试回答一下这个问题:
答:可以,至少可以做到把所有的异常都吃掉。
问:那么怎么做呢?
答:当某个线程发生异常时,只要不让 KillApplicationHandler
处理这个异常就行了,即只要覆盖掉默认的 UncaughtExceptionHandler
就行了噢。
问:那当这样做的时候,比如主线程抛出一个异常被吃掉了,app还能正常运行吗?
答:不能了,因为不做任何处理的话,当前线程的 Looper.loop()
就被终止了。如果是主线程的话,此时你将会获得一个 anr
。
问:怎么才能在吃掉异常的同时,让主线程继续运行呢?
答:当由于异常抛出,导致线程的 Looper.loop()
终止之后,接管 Looper.loop()
。代码大概长下面这样:
public class AppCrashHandler implements UncaughtExceptionHandler { @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) { while (true) { try { if (Looper.myLooper() == null) { Looper.prepare(); } Looper.loop(); } catch (Exception e) { } } } }
上面这段代码,就是我标题中被描述为 Looper 兜底框架的实现机制。但是对于一个正常的app,线上是不可能这样无脑的catch,然后 Looper.loop
的,这是因为:
- 不是所有的异常都需要被catch住,如:
OOM、launcher Activity onCreate
之类的。 - 稳定性不是靠屏蔽问题,而是靠解决问题,当异常无法解决或者解决成本太高,且异常被屏蔽对用户、业务来说并没有啥实质性的影响时,可以被屏蔽,当异常抛出时已经对业务产生了破坏,但是通过保护住然后重试可以让业务恢复运作时,也可以被屏蔽,只是多了个环节,即修复异常。
问:到底什么异常需要被吃掉呢?
上一个回答中我们大致将需要被吃掉的异常分了两类
- 异常我们无法解决或者解决成本太高
举个例子,假如公司有使用 react native 之类的三方大框架,当业务抛出来一个如下的异常时,我们就可以认为这无法解决。
com.facebook.react.bridge.JSApplicationIllegalArgumentException: connectAnimatedNodes: Animated node with tag (child) [30843] does not exist at com.facebook.react.animated.NativeAnimatedNodesManager.connectAnimatedNodes(NativeAnimatedNodesManager.java:7) at com.facebook.react.animated.NativeAnimatedModule$16.execute at com.facebook.react.animated.NativeAnimatedModule$ConcurrentOperationQueue.executeBatch(NativeAnimatedModule.java:7) at com.facebook.react.animated.NativeAnimatedModule$3.execute at com.facebook.react.uimanager.UIViewOperationQueue$UIBlockOperation.execute at com.facebook.react.uimanager.UIViewOperationQueue$1.run(UIViewOperationQueue.java:19) at com.facebook.react.uimanager.UIViewOperationQueue.flushPendingBatches(UIViewOperationQueue.java:10) at com.facebook.react.uimanager.UIViewOperationQueue.access$2600 at com.facebook.react.uimanager.UIViewOperationQueue$DispatchUIFrameCallback.doFrameGuarded(UIViewOperationQueue.java:6) at com.facebook.react.uimanager.GuardedFrameCallback.doFrame(GuardedFrameCallback.java:1) at com.facebook.react.modules.core.ReactChoreographer$ReactChoreographerDispatcher.doFrame(ReactChoreographer.java:7) at com.facebook.react.modules.core.ChoreographerCompat$FrameCallback$1.doFrame at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1118) at android.view.Choreographer.doCallbacks(Choreographer.java:926) at android.view.Choreographer.doFrame(Choreographer.java:854) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1105) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:238) at android.os.Looper.loop(Looper.java:379) at android.app.ActivityThread.main(ActivityThread.java:9271) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1018)
2. 异常被屏蔽对用户、业务来说并没有实质性影响
- 比如老生常谈的 Android 7.x toast的 BadTokenException
之类的系统崩溃,一呢发生概率非常低,二呢在Android 8
上的修复方式也只是 try-catch
住。
- 一些不影响业务、用户使用的三方库崩溃,比如瞎说一个,当使用 OkHttp 在请求接口时,内部切了个线程执行了个更新缓存的任务,结果里面抛出了一个 NPE 。外面没法 try-catch
,而且这个异常抛出时,顶多下次请求不走缓存,实际上没啥太大影响。
3. 异常很严重,但是吃掉之后通过修复运行环境能够让用户所使用的业务恢复正常运行
比如我们想要保存一张图片到磁盘上,但是磁盘满了, 抛出了一个no space left
,这时候我们就可以将异常吃掉,同时清空app的磁盘缓存,并且告知用户重试,就可以成功的让用户保存图片成功
3. 如何Looper兜底框架辅助稳定性治理
我们先明确一下什么崩溃需要通过这种手段来治理、兜底:
系统崩溃,如老生常谈的 Android 7.x toast的 BadTokenException
三方库的无痛崩溃,比如公司有使用 react native
之类的三方大框架,没有能力改或者不想改一些相关的 ui 引起的 崩溃,比如做动画时莫名其妙的抛出异常
一些特殊崩溃,如磁盘空间不足引发的 no space left
,可以尝试通过抓住崩溃同时清理一波app的磁盘缓存,再尝试继续运行。
其他...
问:为什么我的标题中强调了可远程配置化呢?
答:因为可远程配置化能够为框架本身赋能更多。
问:比如?
答:可以提供一种简易的线上容灾机制,假如线上在某个页面发生了一个崩溃,这个崩溃突然发生而且崩溃发生的点本身对业务来说无关紧要(比如有个开发手xx,Integer.parse
整了个汉字,抛异常了),通过热修复来修吧,流程复杂,要改代码、打补丁包、配补丁包。紧急发版吧,成本比热修高了不知多少倍,这时如果有一个可配置化的Looper兜底框架,我通过更新我的配置,保护住这个 Integer.parse 异常,就能很轻松的解决线上问题。
4. 可配置化配置的是什么东西
首先这是一个崩溃保护的框架,那么配置的肯定是能描述崩溃的内容,那么什么东西能描述一个崩溃呢?无非就是以下元素:
- throwable class name
- throwable message
- throwable stacktrace
- Android version
- app version
- model
- brand
- ...
大致就是对崩溃做个标签匹配:这是个什么崩溃,发生在哪个Android版本,发生在哪个App版本,发生在哪个厂商哪个系统版本上。
5. 我们怎么做的
我们的画像标签大致长下面这样:
[ { "class": "", "message": "No space left on device", "stack": [], "app_version": [], "clear_cache": 1, "finish_page": 0, "toast": "", "os_version": [], "model": [] }, { "class": "BadTokenException", "message": "", "stack": [], "app_version": [], "clear_cache": 0, "finish_page": 0, "toast": "", "os_version": [], "model": [] } ]
配置里还加了一些额外的东西,比如:
- 崩溃被保护住的时候,要不要清理下app的缓存
- 崩溃被保护住的时候,要不要弹个 toast 告知用户
- 崩溃被保护住的时候,要不要退出当前页面
就这样,我们的可配置化的Looper兜底框架的全貌就描述完了,最后再总结一下具体的工作流程吧。
Looper兜底流程:
我们会注入自己的 UncaughtExceptionHandler
,当App产生了一个未捕获的异常时,我们通过对这个异常进行几个标签的匹配来判断当前的崩溃是否要进行保护,当需要保护时,接管Looper.loop
,让线程继续运行。
配置更新、生效流程:
当App启动时,拉取远程的崩溃画像配置,当未捕获的异常发生时,读取本地最新的配置,进行标签匹配,如果标签匹配成功,进行Looper兜底。
加载全部内容