一.引言
經(jīng)常在Android的java代碼中動態(tài)設(shè)置布局的讀者應(yīng)該會對動態(tài)獲取控件寬高不陌生落剪,在最近項(xiàng)目中我也有用到。我們知道直接在onCreate
方法中無法通過getWidth
或getHeight
獲取到想要的控件的寬高具體值。因此有幾種方式來獲取,例如在監(jiān)聽中獲取或者通過View.post
的方式獲取。具體方法可以參考下面的這篇文章及穗,寫得比較仔細(xì)。
Activity啟動過程中獲取組件寬高的五種方式
筆者也有使用View.post
來獲取绵载,不過始終對這個方法心存疑問埂陆,為什么一個類似handler.post的方法調(diào)用之后就可以正確得到寬高?通過查看源碼以及查閱的一些資料大致弄懂了流程娃豹,接下來說一下我的分析焚虱,如果有不對的地方歡迎指教~
二.View.post方法的調(diào)用機(jī)制
View的post和postDelay方法其實(shí)是類似的,我們點(diǎn)進(jìn)View.java中的這兩個方法看一下:
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;
}
...
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().postDelayed(action, delayMillis);
return true;
}
這兩個方法中的代碼基本是一致的懂版,不同的只是postDelay中有將delayMillis傳進(jìn)去鹃栽,而在post中最終也是調(diào)用了postDelay這個方法,只是將delayMillis置為0了躯畴,我們可以點(diǎn)進(jìn)getRunQueue().post()
方法中看看:
可以看到確實(shí)如此民鼓。
接下來分析post中具體代碼。代碼比較簡潔私股,我們可以看到其中有對
mAttachInfo
賦值的attachInfo
進(jìn)行判斷摹察,如果為空,則調(diào)用getRunQueue().post()
倡鲸,否則直接返回attachInfo.mHandler.post(action)
。那么這兩個post有什么區(qū)別黄娘?
1.先來看當(dāng)mAttachInfo
不為null時的情況峭状,因?yàn)檫@個較為簡單克滴,點(diǎn)進(jìn)attachInfo.mHandler.post
發(fā)現(xiàn)其實(shí)就是調(diào)用的Handler.post()
。這里就要看看這里的mHandler
是哪里產(chǎn)生的优床。使用AS的ctrl+左鍵一直追根溯源:
final static class AttachInfo {
...
final Handler mHandler;
...
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
mTreeObserver = new ViewTreeObserver(context);
}
可以看到mHandler是AttachInfo的一個變量劝赔,在AttchInfo的構(gòu)造方法中被賦值,查看這個構(gòu)造方法的調(diào)用點(diǎn)(這里可以結(jié)合Source Insight的ctrl+/的查找快捷鍵進(jìn)行查找):
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
可以看到是在ViewRootImpl方法中將mHandler傳入胆敞,查看mHanlder被賦值的地方:
final ViewRootHandler mHandler = new ViewRootHandler();
ViewRootImpl是在主線程中被創(chuàng)建着帽,因此這個handler對象是主線程的handler,至此我們可以知道mAttachInfo
不為空的時候其實(shí)是直接調(diào)用了主線程handler來處理我們post的消息移层。當(dāng)然這里有個問題是為什么使用了主線程的handler來處理消息之后就可以獲取正確的寬高呢仍翰?先別急,我們繼續(xù)梳理了之后來解釋這個問題~
2.上面已經(jīng)分析了mAttachInfo
不為空的情況观话,當(dāng)mAttachInfo
為空時予借,會調(diào)用getRunQueue()
這個方法:
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
返回了一個HandlerActionQueue
的實(shí)例。這個HandlerActionQueue
是什么频蛔,點(diǎn)進(jìn)去看看:
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
public void post(Runnable action) {
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
...
private static class HandlerAction {
final Runnable action;
final long delay;
public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}
public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
}
這個類包含了兩個全局變量灵迫,一個應(yīng)該是用來計(jì)數(shù),另一個mActions
則是一個HandlerAction
數(shù)組晦溪,HandlerAction
中封裝了一個Runnable
對象和一個延時delay
瀑粥。
繼續(xù)看,當(dāng)調(diào)用了post之后三圆,實(shí)際只是生成了一個默認(rèn)長度為4的HandlerAction
數(shù)組狞换,將要實(shí)現(xiàn)的Runnable
傳入。那我們什么時候使用Runnable
呢嫌术?我們可以注意到這個類中有個executeActions
方法:
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;
}
}
這個方法就是將我們post
進(jìn)來的Runnable
使用handler進(jìn)行處理哀澈,可以查看一下這個方法在哪里被調(diào)用以及handler是來自哪里。在View中可以查找到該方法在View.java
類中的dispatchAttachedToWindow
方法中被調(diào)用:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
...
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
...
}
這里可以看到傳入的就是AttachInfo 的mHandler變量度气,上面我們分析過最終mHandler就是主線程的Handler割按,那么現(xiàn)在的問題就是這個info是從哪里傳入的。繼續(xù)追查dispatchAttachedToWindow
方法使用的地方(Source Insight):可以看到總共有四個類有調(diào)用這個方法:AttachInfo_Accessor
磷籍,View
适荣,ViewGroup
,ViewRootImpl
院领。
其中
AttachInfo_Accessor.java
類中沒有我們需要關(guān)注的地方弛矛,View.java
就是當(dāng)前類,顯然沒有被調(diào)用的地方比然,進(jìn)入ViewGroup.java
看看:
@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View view = mTransientViews.get(i);
view.dispatchAttachedToWindow(info,
combineVisibility(visibility, view.getVisibility()));
}
}
可以看到ViewGroup
的dispatchAttachedToWindow
方法中丈氓,調(diào)用了View
的dispatchAttachedToWindow
方法,將父中的AttachInfo
全部傳入子類child中實(shí)現(xiàn)賦值,而此時的child就是View
類万俗,我們不是要查看View
類中的這個方法在哪里調(diào)用嗎湾笛?這里貌似陷入死循環(huán)了。闰歪。沒事嚎研,我們在看最后的ViewRootImpl
方法中有沒有什么信息:
private void performTraversals() {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
在performTraversals
方法中我們看到了這個方法。host是由mView賦值库倘,而mView就是由頂層視圖DecorView所賦值的临扮。這個performTraversals
方法就是用來調(diào)用測量、布局教翩、繪制方法的地方杆勇。mAttachInfo唯一賦值的地方上面我們也有分析過:
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
因此到這里我們就可以梳理一下了:在Activity的DecorView中調(diào)用dispatchAttachedToWindow
時,將mAttachInfo
傳入到View
中迂曲,并調(diào)用mRunQueue.executeActions
的方法靶橱,該方法使用mAttachInfo
的handler
將我們最初調(diào)用View.post(runnable)
方法post進(jìn)來的消息post到消息隊(duì)列中進(jìn)行處理,并且我們知道mAttachInfo
的handler
是主線程的handler
路捧,因此其實(shí)就是post到了主線程的消息隊(duì)列中等待處理关霸。這里我們也就分析完畢。
不過這里有兩個問題:
(1)前面我們知道由mAttachInfo
的值來決定調(diào)用哪個post杰扫。那么mAttachInfo什么時候不為空队寇?查看mAttachInfo
被賦值的地方有兩處:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
...
mAttachInfo = info;
...
}
void dispatchDetachedFromWindow() {
...
mAttachInfo = null;
...
}
從這里我們可以看到在dispatchAttachedToWindow
中mAttachInfo
被賦值為info
,這里的info
不就是從performTraversals
方法中調(diào)用的時候傳入的嗎~所以就是當(dāng)attachedToWindow的時候被賦值章姓,detachedFromWindow時被置為空佳遣。當(dāng)mAttachInfo
為空的時候?qū)?code>Runnable存放到HandlerAction
中,當(dāng)View
的dispatchAttachedToWindow
方法被調(diào)用時使用主線程handler
將其post到消息隊(duì)列中凡伊。當(dāng)mAttachInfo
不為空的時候直接調(diào)用主線程handler
即可零渐。
(2)在performTraversals
方法中我們是先調(diào)用dispatchAttachedToWindow
方法之后才開始調(diào)用measure
和layout
等方法進(jìn)行測量布局的,而在dispatchAttachedToWindow
中我們就有調(diào)用了handler
將消息post到消息隊(duì)列了準(zhǔn)備執(zhí)行了系忙,那此時我們?yōu)槭裁茨軌蛟?code>View.post方法中獲取到正確的長寬呢诵盼?
我們來查看performTraversals
的調(diào)用時機(jī):
void doTraversal() {
...
performTraversals();
...
}
就一處被調(diào)用,繼續(xù)看doTraversal
被調(diào)用的地方:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
我們可以看到其實(shí)它是在一個Runnable
中的run
方法中被調(diào)用银还,這個TraversalRunnable
方法被實(shí)例化之后风宁,
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
分別在scheduleTraversals
和unscheduleTraversals
方法中被調(diào)用,這兩個方法直觀上看上去就是成對出現(xiàn)的方法蛹疯,我們來看scheduleTraversals
方法中有一行代碼:
void scheduleTraversals() {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
這個postCallback
方法貌似也是post
的近親戒财?我們查看Choreographer
中的這個方法可以看到其實(shí)最終就是調(diào)用了Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
方法。這個mHandler
就是主線程Handler
~
因此謎底揭開捺弦,首先在主線程的Handler
中就已經(jīng)post
進(jìn)去了一個Runnable
來執(zhí)行performTraversals
方法饮寞,當(dāng)然就按照順序執(zhí)行了post我們調(diào)用View.post(runnable)
方法到主線程Handler
孝扛、測量、布局骂际、繪制等一系列操作疗琉。然而由于Handler
的機(jī)制冈欢,它是將所有的message都post到一個MessageQueue
中歉铝,按照順序執(zhí)行這些消息。因此只有當(dāng)執(zhí)行完測量凑耻、布局太示、繪制之后,才能執(zhí)行我們的Runnable
香浩,所以我們這時就能夠獲取到正確的寬高了~
三.感想
第一次分析源碼確實(shí)感覺很多知識點(diǎn)都不太理解类缤,不過希望自己能堅(jiān)持下來不斷前行~分析的過程中有借鑒兩位大神的文章,很感謝_
1.【Andorid源碼解析】View.post() 到底干了啥
2.通過View.post()獲取View的寬高引發(fā)的兩個問題