本篇文章分析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的監聽與上層的接收