原始碼詳細解析Activity生命週期onResume中Handler.Post(Runnable)和View.Post(Runnable)的UI效果差異原因
一般需求中會出現在Activity啟動中需要獲取Ui控制元件相關大小或者在介面繪製完成之後重新整理資料,我們都知道在UI繪製完成之後,時機最好,不會阻塞主執行緒導致卡頓或者UI控制元件引數獲取失敗。 也許大家使用過或 知道Handler(MainLooper).Post(Runnable)和View.Post(Runnable)都是把Runnable封裝成Message再 push到主線成中looper中MessageQueue中,會發現在Activity的生命週期中執行這兩種方式效果不同,前者不滿足我們的需求,而後者卻能做到,但這是為啥,有沒有深入分析,本文就從Activity啟動流程以及UI重新整理和繪製流程原理以及訊息迴圈機制、同步障礙機制來剖析。
先看demo執行效果,以獲取Ui控制元件大小為例子,如下:
class MyActivity extends Activity{
.....
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myCustomView = findViewById(R.id.custom);
Log.i("chuan", "onCreate init myCustomView width=" + myCustomView.getWidth());
}
@Override
protected void onResume() {
super.onResume();
Log.i("chuan", "Main onResume");
new Handler().post(new Runnable() {
@Override
public void run() {
Log.i("chuan", "onResume Handler post runnable button width=" + myCustomView.getWidth());
}
});
myCustomView.post(new Runnable() {
@Override
public void run() {
Log.i("chuan", "onResume myCustomView post runnable button width=" + myCustomView.getWidth());
}
});
}
public class MyView extends View {
@Override
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r, b);
Log.i("chuan", "myView layout");
}
@Override
protected void onAttachedToWindow() {
Log.i("chuan", "myView onAttachedToWindow with"+getWidth());
try {
Object mAttachInfo = ReflectUtils.getDeclaredField(this, View.class, "mAttachInfo");
Log.i("chuan", "myView onAttachedToWindow mAttachInfo=null?" + (mAttachInfo == null));
Object mRunQueue = ReflectUtils.getDeclaredField(this, View.class, "mRunQueue");
Log.i("chuan", "myView onAttachedToWindow mRunQueue=null?" + (mRunQueue == null));
} catch (Exception e) {
}
super.onAttachedToWindow();
}
@Override
public boolean post(Runnable action) {
try {
Object mAttachInfo = ReflectUtils.getDeclaredField(this, View.class, "mAttachInfo");
Log.i("chuan", "myView post mAttachInfo=null?" + (mAttachInfo == null));
} catch (Exception e) {
}
return super.post(action);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i("chuan", "myView onDraw");
}
}
日誌顯示結果:
4-19 17:24:05.543 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView init
04-19 17:24:05.545 27745-27745/com.happyelements.AndroidAnimal I/chuan: Main onCreate
04-19 17:24:05.570 27745-27745/com.happyelements.AndroidAnimal I/chuan: onCreate init myCustomView width=0
04-19 17:24:05.578 27745-27745/com.happyelements.AndroidAnimal I/chuan: Main onResume
04-19 17:24:05.577 27745-27745/com.happyelements.AndroidAnimal I/chuan:
myView post mAttachInfo=null?true
04-19 17:24:05.584 27745-27745/com.happyelements.AndroidAnimal I/chuan: onResume Handler post runnable button width=0
04-19 17:24:05.594 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView onAttachedToWindow width=0
myView onAttachedToWindow mAttachInfo=null?false
04-19 17:24:05.630 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView layout
04-19 17:24:05.630 27745-27745/com.happyelements.AndroidAnimal I/chuan: myView onDraw
04-19 17:24:05.631 27745-27745/com.happyelements.AndroidAnimal I/chuan: onResume myCustomView post runnable button width=854
從日誌中可以看出幾點
- 在Activity可互動之前的生命週期中UI直接操作是失效的,即使通過handler把Ui操縱任務post到onResume生命週期之後,也依然獲失效,日誌可以看到此時ui介面都沒有繪製。
- 發現View.post(Runnable)會讓runnable在該View完成了measure、layout、draw之後再執行,這個時候當然就可以獲取到Ui相關引數了。
先看下兩者的原始碼實現:
1、handler.post(Runnable)
Handler.class
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
程式碼簡單可以看到就是把runnable封裝成Message然後加入當前Looper的MessageQueue佇列中。
2、再看下View.post(Runnable)
View.class
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
//先是通過attachInfo.mHandler.post來實現,實際上就是用Handler.post和上述一樣
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
//若attachInfo =null時,維護一個mRunQueue 佇列,然後在dispatchAttachedToWindow通過mRunQueue.executeActions(info.mHandler);跟上述方法一樣
getRunQueue().post(action);
return true;
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
..........省略....
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
.......省略......
}
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
HandlerActionQueue.class
//實際也是通過handler來post到主執行緒
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
本文重點來了:通過原始碼呼叫發現最終都是通過handler.post()方式來加入到主執行緒佇列中,api呼叫一樣為何效果不一樣,下面就從如下幾個知識點來分析:
- Activity生命週期啟動流程
- Message訊息傳送和執行原理機制
- UI繪製重新整理觸發原理機制
- MessageQueue同步障礙機制
Activity啟動流程
這個流程不清楚的,可以網上搜,一大堆。但這裡講的是,ApplicationThread收到AMS的scheduleLaunchActivity的Binder訊息之後,原因是binder執行緒,會通過ActivityThread中的mH(Handler)來sendMessage
private class ApplicationThread extends ApplicationThreadNative {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
....省略....
sendMessage(H.LAUNCH_ACTIVITY, r);
}
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
if (DEBUG_MESSAGES) Slog.v(
TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
+ ": " + arg1 + " / " + obj);
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);
}
}
mH(Handler)會把這個非同步訊息加入到MainLooper中MessageQueue,等到執行時候回撥handleLaunchActivity
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY:
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
handleLaunchActivity方法會執行很多方法,這個是入口,簡單來說會建立Activity物件,呼叫其啟動生命週期,attach、onCreate、onStart、onResume,以及新增到WindowManager中,重點看下本文中onResume生命週期是如何回撥的。在Activity可見之後,緊接著就是要觸發繪製介面了,會走到handleResumeActivity方法,會performResumeActivity呼叫activity的onResume方法
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide) {
ActivityClientRecord r = mActivities.get(token);
if (r != null && !r.activity.mFinished) {
.....................
try {
r.activity.onStateNotSaved();
r.activity.mFragments.noteStateNotSaved();
//呼叫activity的onResume方法
r.activity.performResume();
...............................
} catch (Exception e) {
........
}
}
return r;
}
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//1、呼叫activity的onResume方法
ActivityClientRecord r = performResumeActivity(token, clearHide);
......
if (r != null) {
final Activity a = r.activity;
final int forwardBit = isForward ?
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
.......................
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
//2、decorView先暫時隱藏
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//3、關鍵函式 新增到window觸發ui測量、佈局、繪製
wm.addView(decor, l);
}
..............
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
//4、新增decorView之後,設定可見,從而顯示了activity的介面
r.activity.makeVisible();
}
}
}
由此可見:從handleResumeActivity執行流程來看onResume呼叫時候,Activity中的UI介面並沒有經過measure、layout、draw等流程,所以直接在onResume或者之前的onCreate中執行ui操縱都是無用的,因為這個時候Ui介面不可見,沒有繪製。那為何通過hander.post(Runnable)讓執行發生在handleLaunchActivity這個Message之後,這個時候流程已經走完了,也是在Ui介面觸發繪製之後,怎麼還是不行呢。
Message訊息傳送和執行原理機制這裡就不闡述了,hander.post(Runnable)讓執行發生在handleLaunchActivity這個Message之後就是因為這個Message迴圈機制原理,可以讓任務通常讓加入的先後順序依次執行,所以handleLaunchActivity這個Message執行之後,就是onResume中的push的Message。
但是為何onResume中hander.post(Runnable)還不能ui操作呢,就猜測handleLaunchActivity之後還沒有同步完成UI繪製,那UI繪製重新整理觸發原理機制是怎麼樣的了,直接分析觸發條件,上午中的wm.addVIew開始:windowManager會通過子類WindowManagerImpl來實現,其內部又通過WindowManagerGlobal的單例項來實現addVIew,原始碼如下
WindowManagerGlobal.class
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...............
ViewRootImpl root;
View panelParentView = null;
......
//ViewRootImpl整個UI操作實際控制著
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
.................
// do this last because it fires off messages to start doing things
try {
//繫結decorView,並觸發開發繪製
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
}
}
addView動作又轉給ViewRootImpl.setView來實現,具體原始碼如下:
ViewRootImpl.class
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
..........
//觸發重新整理繪製的關鍵
requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
..........
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
//通過binder call新增到Display
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
} finally {
if (restore) {
attrs.restore();
}
}
...............
//decorView新增父類ViewParent 就是ViewRootImpl
view.assignParent(this);
}
}
}
setView完成了上述幾個重要步驟,其中requestLayout的實現是如何觸發重新整理繪製的:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//安排重新整理請求
scheduleTraversals();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void scheduleTraversals() {
//一個重新整理週期只執行一次即可,遮蔽其他的重新整理請求
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//設定同步障礙Message
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//螢幕重新整理訊號VSYNC 監聽回撥把mTraversalRunnable(執行doTraversal()) push到主執行緒了且是個非同步Message會優先得到執行 ,具體看下Choreographer的實現
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步障礙Message
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//真正執行decorView的繪製
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
.............
Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
.......
mAttachInfo.mUse32BitDrawingCache = true;
mAttachInfo.mHasWindowFocus = false;
mAttachInfo.mWindowVisibility = viewVisibility;
mAttachInfo.mRecomputeGlobalAttributes = false;
//performTraversals 第一次呼叫時候decorView dispatch mAttachInfo變數
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(TAG,
"View " + host + " resized to: " + frame);
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
...................
//會根據狀態判斷是否執行,對mVIew(decorView)執行view的測量、佈局、繪製
perforMeasure()
perforLayout()
perforDraw()
mFirst=false;
}
從上述程式碼可以發現在addView之後同步執行到requestLayout,再到scheduleTraversals中設定了同步障礙訊息,這個簡單闡述,看下原始碼實現:
MessageQueue.class
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
//根據token移動這個Message
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
synchronized (this) {
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
MessageQueue同步障礙機制: 可以發現就是把一條Message,注意這個Message是沒有設定target的,整個訊息迴圈唯一一處不設定回撥的target(hander),因為這個即使標誌了同步障礙訊息,也是不需要handler來pushMessage到佇列中,直接手動迴圈移動連結串列插入到合適time的Message之後的即可。然後是如何識別這個障礙訊息的呢,在Looper的loop迴圈獲取MessageQueue.next()函式獲取下一個的message,是如何實現的,
MessageQueue.class
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//查詢是否有下一個訊息,沒有就阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//關鍵地方,首先識別 msg.target == null情況就是同步障礙訊息,如果該訊息是同步障礙訊息的話,就會迴圈查詢下一個訊息是否是isAsynchronous狀態,非同步Message,專門給重新整理UI訊息使用的
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//如果查到非同步訊息或者沒有設定同步障礙的訊息,直接返回執行
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
........省略.................
nextPollTimeoutMillis = 0;
}
}
可以看到scheduleTraversals中設定了同步障礙訊息,就是相當於在MessageQueue中插入了一個Message,並且是在onResume之後插入的,所以在onResume中handler.post(Runnable)之後,這個訊息會在同步障礙Message之前,會先被執行,這個時候依然沒有重新整理繪製介面,待查詢到同步障礙Message時候,會等待下個非同步Message(重新整理Message)出現。所以在onResume中handler.post(Runnable)是Ui操作失效的。
那麼為何View.post(Runnable)就可以了,再回過頭來看下其原始碼:
View.class
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
由於在onResume中執行,這個時候ViewRootImpl還沒有初始化(addView時),而mAttachInfo是在ViewRootImpl建構函式中初始化的,過此時mAttachInfo=null,從上文知道 getRunQueue()維護了一個mRunQueue 佇列,然後在dispatchAttachedToWindow通過mRunQueue.executeActions(info.mHandler);那這個方法dispatchAttachedToWindow什麼會被呼叫,回顧上文中ViewRootImpl第一次收到Vsync同步重新整理訊號之後會執行performTraversals,這個函式內部做了個判斷當時第一次mFirst時候會呼叫host.dispatchAttachedToWindow(mAttachInfo, 0);
把全域性mAttachInfo下發給所有子View,其原始碼如下:
View.class
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
//向下分發info,其實現在ViewGroup中
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
mWindowAttachCount++;
// We will need to evaluate the drawable state at least once
.........
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
........
}
可以看到這個函式同時執行了 mRunQueue.executeActions(info.mHandler);
從上文可知就是通過hander把mRunQueue中任務全部push到主執行緒中。由此可以知道在performTraversals(Message)中push Message到主線中,肯定會這個performTraversals(Message)之後再執行,並且在doTraversals中移除了同步障礙訊息(Message),故會依次執行。所以onResume中View.post的Message就會在performTraversals之後執行,而performTraversals就是完成了View整個測量、佈局和繪製。當View的mAttachInfo !=null時也說明肯定完成過UI繪製。
感謝看完,看似簡單的東西,其實內部原理沒有分析清楚容易糊塗,同時研究原始碼會學到很多相關的知識點,例如要看懂本文就需要了解上午中提到的4個知識點。
1. Activity生命週期啟動流程
2. Message訊息傳送和執行原理機制
3. UI繪製重新整理觸發原理機制
4. MessageQueue同步障礙機制**