本篇文章分析Android UI刷新机制,就是更新UI,做Android开发初期我们经常会听说不能在子线程更新UI,以及Activity的onCreate方法中获取不到View宽高的问题
我们先来说一下子线程不能更新UI的问题:
public class MainActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
button.setText("button");
}
}).start();
}
});
}
}
我们定义一个button,点击时修改button的文字,发现果然报错:很熟悉的错误,只能在创建视图的原始线程修改视图
03-24 18:17:17.974 25194 27234 E AndroidRuntime: FATAL EXCEPTION: Thread-2
03-24 18:17:17.974 25194 27234 E AndroidRuntime: Process: com.example.myapplication, PID: 25194
03-24 18:17:17.974 25194 27234 E AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
03-24 18:17:17.974 25194 27234 E AndroidRuntime: at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8464)
03-24 18:17:17.974 25194 27234 E AndroidRuntime: at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1468)
我们再把代码改成这样,不在button点击时修改它的text,而在onCreate中修改:
public class MainActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
new Thread(new Runnable() {
@Override
public void run() {
button.setText("button");
}
}).start();
}
}
我们发现这样就不报错了,而且text修改成功,所以我们如果在onCreate中更新UI的话随便在哪个线程都是OK的
为什么会这样?我们看下上面那段抛出的异常,在ViewRootImpl的requestLayout中调用checkThread来检查UI更新的线程的,那么onCreate中能在子线程更新UI,说明此时无法使用checkThread来检查线程,实际上此时ViewRootImpl并没有被创建
ViewRootImpl的创建是在Activity的onResume过程中,在onResume时会通过WindowManagerGlobal的addView方法将Activity的顶层视图DecoreView添加到WMS中,而在WindowManagerGlobal的addView方法中就会创建ViewRootImpl,并调用它的setView方法
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
root = new ViewRootImpl(view.getContext(), display);
...
root.setView(view, wparams, panelParentView);
....
}
ViewRootImpl的setView方法中会调用requestLayout方法,此方法就会调用
checkThread检查当前线程,并进行View的测量,布局,绘制流程
@Override
public void requestLayout() {
...
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
这也解决了为什么onCreate中无法获取View的宽高的问题,因为onResume时才对Activity的布局文件进行测量
好了,在checkThread检查完UI更新的线程是否合法之后就会调用scheduleTraversals触发UI的更新操作了,首先给一个结论,UI的更新不是同步的,而是将UI更新请求首先放入一个callback链表,等待底层的垂直同步脉冲信号(vsync),收到这个信号之后回调callback执行测量,布局,绘制操作,这个信号在底层由硬件发出,有些也是通过软件模拟的,标准情况下是每秒60次,即每秒刷新60帧,也就是我们常说的16.6ms需要完成一帧的绘制,不然就会卡顿
接下来进行代码分析
scheduleTraversals
@UnsupportedAppUsage
void scheduleTraversals() {
//防止重复调用
if (!mTraversalScheduled) {
mTraversalScheduled = true;
.....
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//通知底层即将进行画面渲染
......
}
这个方法里面我们关注两个重点,1.设置同步屏障,2.post一个绘制请求
首先来看设置同步屏障,同步屏障其实就是向MessageQueue添加一个不带target的消息,作用是屏蔽handler的同步消息,只取出异步消息执行,默认handle都是同步消息,
关于同步屏障我在AndroidQ Handler消息机制(java层)这篇文章中详细分析过
接着看调用Choreographer的postCallback方法添加一个Callback,第一个参数是Callback的type,第二个参数是Callback执行的mTraversalRunnable,
我们后面再来看这个Runnable
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
来看看Choreographer的postCallback方法
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
@TestApi
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) {
...
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//(1)步骤1
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//(2)步骤2
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
一通调用,就是一个参数的传递,postCallbackDelayedInternal中我们分两个步骤来分析,先看步骤1:
mCallbackQueues类型为CallbackQueue[],里面有四种类型的CallbackQueue,CallbackQueue是一个链表,就是一系列CALLBACK_XXXX类型请求的集合,按照执行的先后顺序排列,头部是即将执行的请求,我们只关心绘制类型请求,另外还有输入类型,动画类型等
/**
* Callback type: Traversal callback. Handles layout and draw. Runs
* after all other asynchronous messages have been handled.
* @hide
*/
public static final int CALLBACK_TRAVERSAL = 3;
我们传递过来的dueTime为0,需要立即执行,在来看步骤2:
其实如下不管是哪个分支最终都是通过handler发送MSG_DO_SCHEDULE_CALLBACK消息来处理的,只是是否延迟执行而已
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
我们来看scheduleFrameLocked方法
scheduleFrameLocked
private void scheduleFrameLocked(long now) {
//保证执行一次
if (!mFrameScheduled) {
mFrameScheduled = true;
//是否使用VSYNC,现在手机一般都使用的
if (USE_VSYNC) {
...
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//不使用VSYNC的情况
......
}
}
}
上面代码的两个分支最终都是执行同样的方法scheduleVsyncLocked,只不过需要判断当前是否在Looper线程,我们理解成主线程就行了,如果不在就通过Handler消息到主线程执行,我们看Choreographer的代码会发现所有的Handler消息都被设置成了异步消息setAsynchronous(true),这样我们前面开启的同步屏障就起作用了,目的是UI的更新必须以最高优先级进行
scheduleVsyncLocked
@UnsupportedAppUsage
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
mDisplayEventReceiver类型为FrameDisplayEventReceiver,它是上层用来接收Vsync信号的,调用它的scheduleVsync方法向native层注册监听Vsync,需要注意Vsync的接收是注册一次才能接收一次
scheduleVsync
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}
最终通过nativeScheduleVsync向native层注册Vsync的监听,关于Vsync的注册我们下一篇文章来分析
这里我们直接来看接收到Vsync之后的回调
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
......
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
long now = System.nanoTime();
.....
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
......
}
接收到Vsync的回调接着就通过handler到UI线程执行Runnable的回调doFrame
doFrame
@UnsupportedAppUsage
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
//这里有很多关于Vsync到来时间,frame时间计算,以及
//跳frame相关的各种计算我就省略了
.....
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
....
}
最终此方法实际是通过doCallbacks方法执行我们最开始添加的绘制类型的回调的,我们关注传递的type为CALLBACK_TRAVERSAL的回调
doCallbacks
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
//步骤1
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
//不关注这个类型
if (callbackType == Choreographer.CALLBACK_COMMIT) {
.....
}
}
try {
//步骤2
for (CallbackRecord c = callbacks; c != null; c = c.next) {
...
c.run(frameTimeNanos);
}
} finally {
//收尾工作
....
}
}
为了代码简洁,删掉了不关注的部分,上面代码我分为两个部分来分析,先看步骤1:
对type为CALLBACK_TRAVERSAL的CallbackQueues执行extractDueCallbacksLocked方法,主要看看这个方法要干嘛的
extractDueCallbacksLocked
public CallbackRecord extractDueCallbacksLocked(long now) {
CallbackRecord callbacks = mHead;//头指针
//如果链表为空或者链表中头指针指向的callback的执行时间都
//大于当前时间则返回,因为还没到callback执行时间
if (callbacks == null || callbacks.dueTime > now) {
return null;
}
//last是头指针
CallbackRecord last = callbacks;
//next是头指针下一个Callback
CallbackRecord next = last.next;
//这种一看就是遍历链表操作
while (next != null) {
//如果下一个Callback的执行时间大于当前时间
if (next.dueTime > now) {
//则需要断掉链表,因为这里只有头部Callback
//满足执行条件
last.next = null;
break;
}
//否则继续往后查找
last = next;
next = next.next;
}
//链表遍历完成之后,头指针重新指向next,这里的next可能为空,
//为空代表链表所有Callback都需要执行,不为空则指向被断掉的链表
mHead = next;
return callbacks;
}
总结一下extractDueCallbacksLocked的作用,此方法就是为了得到执行时间小于当前时间的Callback链表集合,得到之后会将链表断成两份,前面一份为需要马上执行的Callback,后面一份为还未到执行时间的Callback,这样保证了下一次绘制请求的Callback能够放在后面一个链表中,最早也在下一个Vsync到才执行
好了我们接着看步骤2,步骤2就比较简单了,就是遍历extractDueCallbacksLocked得到的链表,然后依次调用它们的run方法,我们看看CallbackRecord的run方法
private static final class CallbackRecord {
......
@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}
这里的action就是我们最开始scheduleTraversals方法中post进去的,就是这个:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
调用doTraversal方法
doTraversal
void doTraversal() {
.....
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
....
}
}
这个方法先移除了我们开始添加的同步屏障,接着调用我们熟悉的performTraversals方法,开始了测量,布局,绘制流程
到这里UI刷新机制全部分析完了,我们做一个总结:
- 不管什么形式的UI更新,最终都会调用到ViewRootImpl的scheduleTraversals方法,这个方法会开启同步屏障,并且将UI的绘制请求post到一个callback数组中等待执行
- Choreographer中的DisplayEventReceiver会在UI线程中通过Handler异步消息向native层注册一个Vsync信号监听(nativeScheduleVsync)
- 在下一个Vsync到来时会回调FrameDisplayEventReceiver的onVsync方法,并且到UI线程中执行它的run方法,调用doFrame方法
- doFrame中通过doCallbacks获取到执行时间小于当前时间的一条CallbackRecord链表,并遍历链表执行最开始添加的Runnable的run方法
- 这个Runnable中会执行doTraversal,doTraversal中移除同步屏障,并执行performTraversals开始测量,布局,绘制流程
下一篇文章分析Vsync的监听与上层的接收