故事开始
面试官:平时开发中有遇到卡顿问题吗?你一般是如何处理的?
来面试的小伙:额...没有遇到过卡顿问题,我平时写的代码质量比较高,不会出现卡顿。
面试官:...
上面对话像是开玩笑,但是前段时间真的遇到一个来面试的小伙这样答,问他有没有遇到过卡顿问题,一般怎么处理的?他说没遇到过,说他写的代码不会出现卡顿。这回答似乎没啥问题,但是我会认为你在卡顿优化这一块是0经验。
卡顿这个话题,相信大部分两年或以上工作经验的同学都应该能说出个大概。 一般的回答可能类似这样:
卡顿是由于主线程有耗时操作,导致View绘制掉帧,屏幕每16毫秒会刷新一次,也就是每秒会刷新60次,人眼能感觉到卡顿的帧率是每秒24帧。所以解决卡顿的办法就是:耗时操作放到子线程、View的层级不能太多、要合理使用include、ViewStub标签等等这些,来保证每秒画24帧以上。
如果稍微问深一点, 卡顿的底层原理是什么?如何理解16毫秒刷新一次?假如界面没有更新操作,View会每16毫秒draw一次吗?
这个问题相信会难倒一片人,包括大部分3年以上经验的同学,如果没有去阅读源码,未必能答好这个问题。当然,我希望你刚好是小部分人~
接下来将从源码角度分析屏幕刷新机制,深入理解卡顿原理,以及介绍卡顿监控的几种方式,希望对你有帮助。
一、屏幕刷新机制
从 View#requestLayout 开始分析,因为这个方法是主动请求UI更新,从这里分析完全没问题。
1. View#requestLayout
protected ViewParent mParent; ... public void requestLayout() { ... if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); //1 } }
主要看注释1,这里的 mParent.requestLayout(),最终会调用 ViewRootImpl 的 requestLayout 方法。你可能会问,为什么是ViewRootImpl?因为根View是DecorView,而DecorView的parent就是ViewRootImpl,具体看ViewRootImpl的setView方法里调用view.assignParent(this);,可以暂且先认为就是这样的,之后整理View的绘制流程的时候会详细分析。
2. ViewRootImpl#requestLayout
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { //1 检测线程 checkThread(); mLayoutRequested = true; //2 scheduleTraversals(); }}
注释1 是检测当前是不是在主线程
2.1 ViewRootImpl#checkThread
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); }}
这个异常很熟悉吧,我们平时说的子线程不能更新UI,会抛异常,就是在这里判断的,ViewRootImpl#checkThread
接着看注释2
2.2 ViewRootImpl#scheduleTraversals
void scheduleTraversals() { //1、注意这个标志位,多次调用 requestLayout,要这个标志位false才有效 if (!mTraversalScheduled) { mTraversalScheduled = true; // 2. 同步屏障 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 3. 向 Choreographer 提交一个任务 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } //绘制前发一个通知 notifyRendererOfFramePending(); //这个是释放锁,先不管 pokeDrawLockIfNeeded(); }}
主要看注释的3点:
注释1:防止短时间多次调用 requestLayout 重复绘制多次,假如调用requestLayout 之后还没有到这一帧绘制完成,再次调用是没什么意义的。
注释2: 涉及到Handler的一个知识点,同步屏障: 往消息队列插入一个同步屏障消息,这时候消息队列中的同步消息不会被处理,而是优先处理异步消息。这里很好理解,UI相关的操作优先级最高,比如消息队列有很多没处理完的任务,这时候启动一个Activity,当然要优先处理Activity启动,然后再去处理其他的消息,同步屏障的设计堪称一绝吧。 同步屏障的处理代码在MessageQueue的next方法:
Message next() {... for (;;) { ... synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null && msg.target == null) { //如果msg不为空并且target为空 // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } ...}
逻辑就是:如果msg不为空并且target为空,说明是一个同步屏障消息,进入do while循环,遍历链表,直到找到异步消息msg.isAsynchronous()才跳出循环交给Handler去处理这个异步消息。
回到上面的注释3:mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);,往Choreographer 提交一个任务 mTraversalRunnable,这个任务不会马上就执行,接着看~
3. Choreographer
看下 mChoreographer.postCallback
3.1 Choreographer#postCallback
public void postCallback(int callbackType, Runnable action, Object token) { postCallbackDelayed(callbackType, action, token, 0);}public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) { if (action == null) { throw new IllegalArgumentException("action must not be null"); } if (callbackType < 0 || callbackType > CALLBACK_LAST) { throw new IllegalArgumentException("callbackType is invalid"); } postCallbackDelayedInternal(callbackType, action, token, delayMillis);}private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { if (DEBUG_FRAMES) { Log.d(TAG, "PostCallback: type=" + callbackType +