Android稳定性:可远程配置化的Looper兜底框架
本文作者
作者:邹阿涛涛涛涛涛涛涛涛
链接:
https://juejin.cn/post/7198466997288566842
本文由作者授权发布。
代码 demo
https://github.com/scuzoutao/AndroidCrashProtect
App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。相关数据显示,当Android App的崩溃率超过0.4%的时候,活跃用户有明显下降态势。
这个问题问出来的前提是指发生的崩溃是来自于 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) {
}
}
}
}
不是所有的异常都需要被catch住,如:OOM、launcher Activity onCreate之类的。 稳定性不是靠屏蔽问题,而是靠解决问题,当异常无法解决或者解决成本太高,且异常被屏蔽对用户、业务来说并没有啥实质性的影响时,可以被屏蔽,当异常抛出时已经对业务产生了破坏,但是通过保护住然后重试可以让业务恢复运作时,也可以被屏蔽,只是多了个环节,即修复异常。
private void testCrash() {
int x = 0;
if(x == 0){
throw new IllegalArgumentException("xx");
}
int y = 1;
Log.e("TEST", "y is : " + y);
}
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)
比如我们想要保存一张图片到磁盘上,但是磁盘满了, 抛出了一个no space left,这时候我们就可以将异常吃掉,同时清空app的磁盘缓存,并且告知用户重试,就可以成功的让用户保存图片成功。
系统崩溃,如老生常谈的 Android 7.x toast的 BadTokenException。 三方库的无痛崩溃,比如公司有使用 react native 之类的三方大框架,没有能力改或者不想改一些相关的 ui 引起的 崩溃,比如做动画时莫名其妙的抛出异常。 一些特殊崩溃,如磁盘空间不足引发的 no space left,可以尝试通过抓住崩溃同时清理一波app的磁盘缓存,再尝试继续运行。 其他...
public class MyApplication extends Application{
@override
public void onCreate(){
super.onCreate();
CrashProtectUtil.init();
}
}
public class CrashProtectUtil{
public void init() {
mOldHandler = Thread.getDefaultUncaughtExceptionHandler();
if (mOldHandler != this) {
Thread.setDefaultUncaughtExceptionHandler(this);
}
}
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
//判断异常是否需要兜底
if (needBandage(ex)) {
bandage();
return;
}
//崩吧
if (mOldHandler != null) {
mOldHandler.uncaughtException(thread, ex);
}
}
private boolean needBandage(Throwable ex) {
//如果是没磁盘空间了,尝试清理一波缓存
if (isNoSpaceException(ex)) {
CacheCleaner.cleanAppCache(mContext, true);
return true;
}
//BadTokenException
if (isBadTokenException(ex)) {
return true;
}
return false;
}
private boolean isNoSpaceException(Throwable ex) {
String message = ex.getMessage();
return !TextUtils.isEmpty(message) && message.contains("No space left on device");
}
private boolean isBadTokenException(Throwable ex) {
return ex instanceof WindowManager.BadTokenException;
}
/**
* 让当前线程恢复运行
*/
private void bandage() {
try {
//fix No Looper; Looper.prepare() wasn't called on this thread.
if (Looper.myLooper() == null) {
Looper.prepare();
}
Looper.loop();
} catch (Exception e) {
uncaughtException(Thread.currentThread(), e);
}
}
}
答:可以提供一种简易的线上容灾机制,假如线上在某个页面发生了一个崩溃,这个崩溃突然发生而且崩溃发生的点本身对业务来说无关紧要(比如有个开发手贱,Integer.parse整了个汉字,抛异常了),通过热修复来修吧,流程复杂,要改代码、打补丁包、配补丁包。紧急发版吧,成本比热修高了不知多少倍,这时如果有一个可配置化的Looper兜底框架,我通过更新我的配置,保护住这个 Integer.parse 异常,就能很轻松的解决线上问题。
throwable class name throwable message throwable stacktrace Android version app version model brand ...
大致就是对崩溃做个标签匹配:这是个什么崩溃,发生在哪个Android版本,发生在哪个App版本,发生在哪个厂商哪个系统版本上。
[ { "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 告知用户。 崩溃被保护住的时候,要不要退出当前页面。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!