https://mp.weixin.qq.com/s/3dubi2GVW_rVFZZztCpsKg
卡顿 UI线程不能够及时的进行渲染,导致UI的反馈不能按照用户的预期,连续、一致的呈现。
ANR ANR是Google人为规定的概念,产生ANR的原因最多也只有四个。
二、Looper Printer
而大部分的主线程的操作最终都会执行到这个dispatchMessage方法中。
为什么说是大部分?因为有些情况的卡顿,这种方案从原理上就无法监控到。
下图是next方法简化过后的源码,frameworks/base/core/java/android/os/MessageQueue.java:
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
//......
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
//......
}
如果排除主线程空闲的情况,究竟会是什么原因会卡在MessageQueue的next方法中呢?
1.除了主线程空闲时就是阻塞在nativePollOnce之外,非常重要的是,应用的Touch事件也是在这里被处理的。这就意味着,View的TouchEvent中的卡顿这种方案是无法监控的。然而,对于我们来说,微信中有大量的自定义View,这些View中充满了各种各样很多的onTouch回调,卡在这里面的情况非常普遍,这种情况的卡顿监控不到是很难接受的。
2.另外一种常见的情况是IdleHandler的queueIdle()回调方法也是无法被监控的,这个方法会在主线程空闲的时候被调用。然而实际上,很多开发同学都先入为主的认为这个时候反正主线程空闲,做一些耗时操作也没所谓。其实主线程MessageQueue的queueIdle默认当然也是执行在主线程中,所以这里的耗时操作其实是很容易引起卡顿和ANR的。例如微信之前就使用IdleHandler在进入微信的主界面后,做一些读写文件的IO操作,就造成了一些卡顿和ANR问题。'
3.还有一类相对少见的问题是SyncBarrier(同步屏障)的泄漏同样无法被监控到,当我们每次通过invalidate来刷新UI时,最终都会调用到ViewRootImpl中的scheduleTraversals方法,会向主线程的Looper中post一个SyncBarrier,其目的是为了在刷新UI时,主线程的同步消息都被跳过,此时渲染UI的异步消息就可以得到优先处理。但是我们注意到这个方法是线程不安全的,如果在非主线程中调用到了这里,就有可能会同时post多个SyncBarrier,但只能remove掉最后一个,从而有一个SyncBarrier就永远无法被remove,就导致了主线程Looper无法处理同步消息(Message默认就是同步消息),导致卡死,参考源码frameworks/base/core/java/android/view/ViewRootImpl.java:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
3.1. 监控IdleHandler卡顿
我们惊喜的发现MessageQueue中的mIdleHandlers是可以被反射的,这个变量保存了所有将要执行的IdleHandler,我们只需要把ArrayList类型的mIdleHandlers,通过反射,替换为MyArrayList,在我们自定义的MyArrayList中重写add方法,再将我们自定义的MyIdleHandler添加到MyArrayList中,就完成了“偷天换日”。从此之后MessageQueue每次执行queueIdle回调方法,都会执行到我们的MyIdleHandler中的的queueIdle方法,就可以在这里监控queueIdle的执行时间了。
3.2. 监控TouchEvent卡顿
熟悉input系统的同学应该知道,Touch事件最终是通过server端的InputDispatcher线程传递给Client端的UI线程的,并且使用的是一对Socket进行通讯的。我们可以通过PLT Hook,去Hook这对Socket的send和recv方法来监控Touch事件啊!我们先捋一下一次Touch事件的处理过程:
我们通过PLT Hook,成功hook到libinput.so中的recvfrom和sendto方法,使用我们自己的方法进行替换。当调用到了recvfrom时,说明我们的应用接收到了Touch事件,当调用到了sendto时,说明这个Touch事件已经被成功消费掉了,当两者的时间相差过大时即说明产生了一次Touch事件的卡顿。这种方案经过验证是可行的!
3.3. 监控SyncBarrier泄漏
最后,SyncBarrier泄漏的问题,有什么好办法能监控到吗?目前我们的方案是不断轮询主线程Looper的MessageQueue的mMessage(也就是主线程当前正在处理的Message)。而SyncBarrier本身也是一种特殊的Message,其特殊在它的target是null。如果我们通过反射mMessage,发现当前的Message的target为null,并且通过这个Message的when发现其已经存在很久了,这个时候我们合理怀疑产生了SyncBarrier的泄漏(但还不能完全确定,因为如果当时因为其他原因导致主线程卡死,也可能会导致这种现象),然后再发送一个同步消息和一个异步消息,如果异步消息被处理了,但是同步消息一直无法被处理,这时候就说明产生了SyncBarrier的泄漏。如果激进一些,这个时候我们甚至可以反射调用MessageQueue的removeSyncBarrier方法,手动把这个SyncBarrier移除掉,从而从错误状态中恢复。下面代码展示了大概的原理:
MessageQueue mainQueue = Looper.getMainLooper().getQueue();
Field field = mainQueue.getClass().getDeclaredField("mMessages");
field.setAccessible(true);
Message mMessage = (Message) field.get(mainQueue); //通过反射得到当前正在等待执行的Message
if (mMessage != null) {
currentMessageToString = mMessage.toString();
long when = mMessage.getWhen() - SystemClock.uptimeMillis();
if (when < -3000 && mMessage.getTarget() == null) { //target == null则为sync barrier
int token = mMessage.arg1;
startCheckLeaking(token);
}
}
private static void startCheckLeaking(int token) {
int checkCount = 0;
barrierCount = 0;
while (checkCount < CHECK_STRICTLY_MAX_COUNT) {
checkCount++;
int latestToken = getSyncBarrierToken();
if (token != latestToken) { //token变了,不是同一个barrier,return
break;
}
if (DetectSyncBarrierOnce()) {
//发生了sync barrier泄漏
removeSyncBarrier(token); //手动remove泄漏的sync barrier
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void removeSyncBarrier(int token) {
MessageQueue mainQueue = Looper.getMainLooper().getQueue();
Method method = mainQueue.getClass().getDeclaredMethod("removeSyncBarrier", int.class);
method.setAccessible(true);
method.invoke(mainQueue, token);
}
private static boolean DetectSyncBarrierOnce() {
Handler mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.arg1 == 0) {
barrierCount ++; //收到了异步消息,count++
} else if (msg.arg1 == 1) {
barrierCount = 0; //收到了同步消息,说明同步屏障不在, count设置为0
}
}
};
Message asyncMessage = Message.obtain();
asyncMessage.setAsynchronous(true);
asyncMessage.setTarget(mainHandler);
asyncMessage.arg1 = 0;
Message syncNormalMessage = Message.obtain();
syncNormalMessage.arg1 = 1;
mainHandler.sendMessage(asyncMessage); //发送一个异步消息
mainHandler.sendMessage(syncNormalMessage); //发送一个同步消息
if(barrierCount > 3){
return true;
}
return false;
}
坏消息是,这种方案只能监控到问题的产生,也可以直接解决问题,但是无法溯源问题究竟是哪个View导致的。其实我们也尝试过,通过插桩或者Java hook的方法,监控invalidate方法是否在非主线程中进行,但是考虑到风险以及对性能影响都比较大,没有在线上使用。所幸,通过监控发现,这个问题对我们来说,发生的概率并不高。如果发现某个场景下该问题确实较为严重,可以考虑使用插桩或者Java hook在测试环境下debug该问题。