故事开始
面试官:平时开发中有遇到卡顿问题吗?你一般是如何处理的?
来面试的小伙:额…没有遇到过卡顿问题,我平时写的代码质量比较高,不会出现卡顿。
面试官:…
上面对话像是开玩笑,但是前段时间真的遇到一个来面试的小伙这样答,问他有没有遇到过卡顿问题,一般怎么处理的?他说没遇到过,说他写的代码不会出现卡顿。这回答似乎没啥问题,但是我会认为你在卡顿优化这一块是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
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//1.将任务添加到队列
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//2\. 正常延时是0,走这里
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
//3\. 什么时候会有延时,绘制超时,等下一个vsync?
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callba