背景
在 开发 Android 应用过程中,在 Looper 的 handleCallback 的方法中经常会发生一些异常而导致应用崩溃,其实很多异常是没必要导致整个应用崩溃的。但是我们无法直接对进行 try catch 处理,也无法得知这些 message/runnable 是在哪里被 post 到主线程的消息队列中的,这些 bug 常常占据应用崩溃率指标的一部分,影响用户体验。
问题分析
应用启动的时候,在系统的ActiivtyThread的 main 方法中会调用 Looper#loop 方法开启一个无限循环不断从 Looper 的 MessageQueue 中 取出 Message 交给 Handler 来处理, 伪代码如下:
#Looper
public static void loop() {
final Looper me = myLooper();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
return;
}
....
try {
// 这里实际上是 Handler#dispatchMessage(msg)
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
}
....
}
}
}
主线程Looper 的handleCallback 过程都是经过下面这句代码调用的:
//Handler#dispatchMessage(msg)
msg.target.dispatchMessage(msg);
实际上, Android 的生命周期函数的调用以及平常我们通过 Handler post 的 Message 和 Runnable 实际上都是通过Looper 往消息队列 post 消息的机制来完成的。
因此如果我们能对以上 主线程 Handler 的 dispatchMessage 做 try catch 处理,就能间接地忽略掉那些我们认为没必要导致系统崩溃的异常了。
方案探索
要想针对Looper的handleMessage中的某些异常做处理,实际上就是需要能找到处理Handler处理handleMessage 的 "勾子"函数,就可以对其进行异常处理了 ,有以下两种办法:
- 反射 Hook 所有 Handler 和 Looper 操作
使用反射的方式 对 Handler 的 handleMessage 方法做反射处理,替换成自定义的 Handler,在自定义的 HandleMessage 方法中对相应的指定的异常类型做 try catch 处理。
缺点:在不同厂商不同 Android 版本设备存在兼容性问题,不排除未来在某个 Android 版本中无法反射而导致机制失效
- 接管 Looper
在系统 ActivityThread 启动主线程的时候,系统会开启Looper循环,死循环不断从消息队列中取出消息来执行 Handler#handleMessage 方法。只要能
- 接管 Looper
优点:不需要反射,没有兼容性问题
接管Looper方案实现
我们可以在应用 Application 启动的时候,通过主线程 Handler往其中 post 一个无限循环的 Runnable,然后在 Runnable 内部调用 Looper.loop() 方法,即可让原来的 Looper 保存运转,同时拥有了 try-catch 的机会。
image.png
代码实现:
private void initLoop(){
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(new Runnable() {
@Override
public void run() {
while (true){
try{
Looper.loop();
}catch (Exception e){
e.printStackTrace();
String stack = Log.getStackTraceString(e);
if (e instanceof SecurityException){
}
else if (e instanceof WindowManager.BadTokenException){
} else if (e instanceof IndexOutOfBoundsException){
}
else if (
stack.contains("SelectionHandleView")
|| stack.contains("Magnifier.show")
|| stack.contains("ViewRootImpl.handleDragEvent")
....
) {
e.printStackTrace();
}else {
throw e;
}
}
}
}
});
}
结语
这种接管系统 Looper 的方案,不需要任何反射、没有任何兼容性问题,也不会引起系统异常。对于那些 try-catch 不会影响上下文混乱的异常,我们都可以采用这种方式来使我们的应用更加稳定。
参考
本文技术方案参考了来自 Drakeet 大神 在 [扔物线] 知识星球的分享
网友评论