https://mp.weixin.qq.com/s/3dubi2GVW_rVFZZztCpsKg
卡顿 UI线程不能够及时的进行渲染,导致UI的反馈不能按照用户的预期,连续、一致的呈现。
ANR ANR是Google人为规定的概念,产生ANR的原因最多也只有四个。
下图是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事件的卡顿。这种方案经过验证是可行的!
最后,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该问题。