ps:源碼是基于 android api 27 來(lái)分析的克锣,demo 是用 kotlin 語(yǔ)言寫(xiě)的。
在Android
中蕊程,如果在Activity
的onCreate
方法直接獲取View
的寬高是獲取不到的,但如果是調(diào)用 View
的post
方法通過(guò)它(post方法)參數(shù)Runnable
接口的回調(diào)卻能夠獲取到 View
的寬高袜腥,View
的 post
方法是馬上執(zhí)行的嗎肩狂?它的執(zhí)行時(shí)機(jī)又是什么時(shí)候呢摘完?下面我們舉個(gè)例子驗(yàn)證一下以下幾點(diǎn):
-
post
方法中的Runnable
接口的回調(diào)能否直接獲取View
的寬; -
post
方法中Runnable
接口的回調(diào)和Activity
的onResume
方法的先后順序; -
Activity
的onResume
方法能否直接獲取View
的寬; -
View
沒(méi)有被添加到Window
里的時(shí)候姥饰,執(zhí)行post
方法,Runnable
接口能否被回調(diào)傻谁。
PostDemo
(1)新建一個(gè) Kotlin 類(lèi)型的類(lèi)MyView
并繼承于View
:
class MyView: View {
constructor(context: Context): super(context) {
}
constructor(context: Context,@Nullable attrs: AttributeSet): super(context,attrs) {}
constructor(context: Context,@Nullable attrs: AttributeSet,defStyleAttr: Int): super(context,attrs,defStyleAttr) { }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
Log.d(MainActivity.TAG,"------onMeasure--")
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
Log.d(MainActivity.TAG,"------onLayout--")
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
Log.d(MainActivity.TAG,"------onDraw--")
}
}
(2)新建一個(gè) Kotlin 語(yǔ)言類(lèi)型的Activity
,名叫MainActivity
:
class MainActivity: AppCompatActivity() {
companion object {
var TAG: String = "MainActivity"
}
var mView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mView = findViewById(R.id.my_view);
mView?.post(Runnable {
Log.d(TAG,"在 post 方法中獲取 View 的寬--" + mView?.getWidth())
})
}
override fun onResume() {
super.onResume()
Log.d(TAG,"----onResume---")
Log.d(TAG,"在 onResume 方法中獲取 View 的寬--" + mView?.getWidth())
}
}
(3)MainActivity
的布局界面activity_main.xml
如下所示:
<?xml version="1.0" encoding="utf-8"?>
<com.xe.postdemo.MyView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/my_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0000"
tools:context="com.xe.postdemo.MainActivity">
</com.xe.postdemo.MyView>
把程序運(yùn)行一下列粪,日志打印如下所示:
08-28 08:54:24.364 10133-10133/com.xe.postdemo D/MainActivity: ----onResume---
08-28 08:54:24.364 10133-10133/com.xe.postdemo D/MainActivity: 在 onResume 方法中獲取 View 的寬--0
08-28 08:54:24.435 10133-10133/com.xe.postdemo D/MainActivity: ------onMeasure--
08-28 08:54:24.487 10133-10133/com.xe.postdemo D/MainActivity: ------onMeasure--
08-28 08:54:24.490 10133-10133/com.xe.postdemo D/MainActivity: ------onLayout--
08-28 08:54:24.501 2285-2738/? D/Launcher.AllAppsList: updatePackage, find appInfo=AppInfo, id=-1, itemType=0, user=UserHandle{0}, mIconType=0, pkgName=com.xe.postdemo, className=com.xe.postdemo.MainActivity, screenId=-1, container=-1, cellX=-1, cellY=-1, spanX=1, spanY=1, isLandscapePos=false from ComponentInfo{com.xe.postdemo/com.xe.postdemo.MainActivity}
08-28 08:54:24.505 2285-2738/? D/Launcher.Model: onReceiveBackground, mAllAppsList=add=[], remove=[], modified=[(0, AppInfo, id=-1, itemType=0, user=UserHandle{0}, mIconType=0, pkgName=com.xe.postdemo, className=com.xe.postdemo.MainActivity, screenId=-1, container=-1, cellX=-1, cellY=-1, spanX=1, spanY=1, isLandscapePos=false)]
08-28 08:54:24.542 10133-10133/com.xe.postdemo D/MainActivity: ------onDraw--
08-28 08:54:24.714 1472-1555/? I/Timeline: Timeline: Activity_windows_visible id: ActivityRecord{afbc1c9 u0 com.xe.postdemo/.MainActivity t7087} time:4018603
08-28 08:54:24.715 1472-1530/? I/ActivityManager: Displayed com.xe.postdemo/.MainActivity: +4s904ms
08-28 08:54:24.762 10133-10133/com.xe.postdemo D/MainActivity: 在 post 方法中獲取 View 的寬--720
從日志可以看出:
post
方法中Runnable
接口的回調(diào)中是可以直接獲取到View
的审磁;
Activity
的onResume
方法比post
方法中Runnable
接口的回調(diào)先執(zhí)行,post
方法中Runnable
接口是在View
的繪制(主要是View
的onMeasure
岂座、onLayout
和onDraw
方法)之后才會(huì)被回調(diào)态蒂;
在Activity
的onResume
方法不能直接獲取View
的寬,因?yàn)?code>Activity 的onResume
方法比View
的onMeasure
和onLayout
方法先執(zhí)行费什。
好钾恢,我們把MainActivity
的onCreate
方法做一下修改,其他不變:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
// mView = findViewById(R.id.my_view);
mView = MyView(this)
mView?.post(Runnable {
Log.d(TAG,"在 post 方法中獲取 View 的寬--" + mView?.getWidth())
})
}
運(yùn)行程序鸳址,日志打印如下所示:
08-31 13:24:02.481 10794-10794/com.xe.postdemo D/MainActivity: ----onResume---
08-31 13:24:02.481 10794-10794/com.xe.postdemo D/MainActivity: 在 onResume 方法中獲取 View 的寬--0
從日志可以看出:
View
沒(méi)有被添加到Window
里的時(shí)候瘩蚪,執(zhí)行post
方法,Runnable
接口不會(huì)被回調(diào)稿黍。
好了疹瘦,以上驗(yàn)證的4點(diǎn)以及View
的post
方法執(zhí)行時(shí)機(jī)可以從源碼中找到原因,我們先從View
的post
方法看起巡球;
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;
}
這里attachInfo
是為空的言沐,所以不會(huì)執(zhí)行 attachInfo.mHandler.post(action)
這行代碼,我們看 getRunQueue().post(action)
這行代碼酣栈,getRunQueue()
方法得到的是一個(gè)HandlerActionQueue
類(lèi)型的對(duì)象险胰,我們點(diǎn)擊HandlerActionQueue
的post
方法查看;
public void post(Runnable action) {
postDelayed(action, 0);
}
HandlerActionQueue
的post
方法又調(diào)用了自己的postDelayed
方法矿筝,這里的參數(shù)0表示延時(shí)所有的事件起便,我們往下看HandlerActionQueue
的postDelayed
方法;
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++;
}
}
從這個(gè)方法看出:HandlerAction
表示要執(zhí)行的任務(wù)跋涣,要執(zhí)行的任務(wù)HandlerAction
保存在數(shù)組長(zhǎng)度為4的mActions
數(shù)組中缨睡,mCount
表示數(shù)組mActions
的下標(biāo),每次都加1陈辱;這個(gè)postDelayd
方法并沒(méi)有馬上執(zhí)行任務(wù)奖年,而是保存了任務(wù),那么執(zhí)行任務(wù)的語(yǔ)句在哪里呢沛贪?
有時(shí)候我們會(huì)說(shuō)看到Activity
的界面后 onResume
方法就會(huì)被回調(diào)陋守,所以我們從調(diào)用Activity
的onResume
方法的AcitivityThread.handleResumeActivity
方法說(shuō)起震贵;
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
......
//1、
// TODO Push resumeArgs into the activity for consideration
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
......
if (!r.activity.mFinished && willBeVisible
......
if (r.activity.mVisibleFromClient) {
//2水评、
r.activity.makeVisible();
}
}
......
} else {
......
}
}
這里的注釋1 最終會(huì)調(diào)用Activity
的onResume
方法猩系,
我們往下看注釋2 的代碼,它是Activity
的makeVisible
方法:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
//3中燥、
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
注釋3 表示將View
視圖添加到ViewManager
中寇甸,我們點(diǎn)擊注釋3 的代碼進(jìn)去看看,ViewManager
的實(shí)現(xiàn)類(lèi)是WindowManagerImpl
疗涉,所以我們看的是WindowManagerImpl
的addView
方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//4拿霉、
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
注釋4 處調(diào)用了WindowManagerGlobal
的addView
方法,我們往下看咱扣;
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
synchronized (mLock) {
......
//5绽淘、
root = new ViewRootImpl(view.getContext(), display);
......
try {
//6、
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
......
}
}
}
我們點(diǎn)擊注釋5 的代碼闹伪,看看ViewRootImpl
的構(gòu)造方法沪铭;
public ViewRootImpl(Context context, Display display) {
......
//7、
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
......
}
一剛開(kāi)始的時(shí)候偏瓤,我們?cè)?code>Activity的onCreate
方法中先執(zhí)行View.post
方法杀怠,但是ViewRootImpl
在Activity
的onResume
方法之后才會(huì)被初始化,還順便在ViewRootImpl
的構(gòu)造方法中初始化AttachInfo
硼补,所以說(shuō)一開(kāi)始View.post
方法中的attachInfo
就為null
驮肉,從而執(zhí)行getRunQueue().post(action)
語(yǔ)句;
我們往下看注釋6 的代碼已骇,也就是ViewRootImpl
的setView
方法离钝;
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//8、
mView = view;
......
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
//9褪储、
requestLayout();
......
}
}
}
這里注釋8 的View
是底部容器DecorView
卵渴;
我們繼續(xù)往下看注釋9 的方法,也就是ViewRootImpl
的requestLayout
方法鲤竹;
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
requestLayout
方法調(diào)用了ViewRootImpl
的scheduleTraversals
方法浪读,我們且看scheduleTraversals
方法;
void scheduleTraversals() {
if (!mTraversalScheduled) {
......
//10辛藻、
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//11碘橘、
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
注釋10 這里先不說(shuō),后面再說(shuō)吱肌,
注釋11 的代碼會(huì)回調(diào)mTraversalRunnable
對(duì)象痘拆,而mTraversalRunnable
對(duì)象是一個(gè)Runnable
接口的實(shí)現(xiàn)類(lèi)TraversalRunnable
具體的對(duì)象,mTraversalRunnable
對(duì)象又調(diào)用了ViewRootImpl
的doTraversal
方法氮墨,doTraversal
方法又調(diào)用了ViewRootImpl
的performTraversals
方法纺蛆,我們來(lái)看看performTraversals
方法:
private void performTraversals() {
//12
// cache mView since it is used so much below...
final View host = mView;
......
if (mFirst) {
......
//13吐葵、
host.dispatchAttachedToWindow(mAttachInfo, 0);
......
} else {
......
}
......
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
mForceNextWindowRelayout = false;
......
if (!mStopped || mReportNextDraw) {
......
//14、
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
} else {
......
}
......
if (didLayout) {
//15桥氏、
performLayout(lp, mWidth, mHeight);
......
}
......
if (!cancelDraw && !newSurface) {
......
//16温峭、
performDraw();
} else {
......
}
......
}
注釋12 就是上面注釋8 中說(shuō)的底部容器DecorView
,
注釋13 表示DecorView
關(guān)聯(lián)AttachInfo
字支,dispatchAttachedToWindow
方法是在ViewGroup
里實(shí)現(xiàn)凤藏,該方法會(huì)遍歷DecorView
的子元素進(jìn)行 關(guān)聯(lián)AttachInfo
,我們看一下ViewGroup
的dispatchAttachedToWindow
方法:
@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
......
for (int i = 0; i < count; i++) {
final View child = children[i];
//17祥款、
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
......
}
這個(gè)方法是很重要的:
子元素關(guān)聯(lián)了AttachInfo
清笨,然后將之前 View.post
保存的任務(wù)添加到AttachInfo
內(nèi)部的Handler
,所以View
沒(méi)有被添加到Window
里的時(shí)候刃跛,執(zhí)行post
方法,Runnable
接口沒(méi)有被回調(diào)苛萎;
我們看注釋17 的代碼桨昙,它是View
的dispatchAttachedToWindow
方法;
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
......
// Transfer all pending runnables.
if (mRunQueue != null) {
//18腌歉、
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
......
}
注釋18 的代碼表示調(diào)用了HandlerActionQueue
的executeActions
方法蛙酪,我們來(lái)看看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];
//19翘盖、
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
//20桂塞、
mActions = null;
mCount = 0;
}
}
注釋19 行的代碼我們先留下懸念;
注釋20 是將mActions
置空馍驯,從第二次調(diào)用 View.post
開(kāi)始阁危,Runnable
會(huì)被添加到AttachInfo
內(nèi)部的Handler
,而不是HandlerAction
汰瘫,View
的onMeasure
狂打、onLayout
和onDraw
方法也不會(huì)被調(diào)用;
我們回過(guò)頭來(lái)看注釋14 的代碼混弥,也就是ViewRootImpl
的performMeasure
方法趴乡;
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
......
try {
//21、
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
注釋21 的mView
表示DecorView
蝗拿,它的measure
方法是在View
里晾捏,我們來(lái)看一下View
的measure
方法;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
if (forceLayout || needsLayout) {
......
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
//22哀托、
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
......
}
......
}
......
}
注釋22 的代碼是調(diào)用的是DecorView
的onMeasure
方法惦辛,而不是View
的onMeasure
方法,我們往下看DecorView
的onMeasure
方法萤捆;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
DecorView
的onMeasure
方法調(diào)用了 父類(lèi)的onMeasure
方法裙品,最終的實(shí)現(xiàn)是在FrameLayout
中measure
方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//23俗批、
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
......
}
}
......
}
注釋23 的代碼會(huì)對(duì)所有子View
進(jìn)行了一遍測(cè)量,并計(jì)算出所有子 View 的最大寬度和最大高度市怎,我們往下看FrameLayout
的measureChildWithMargins
方法岁忘;
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
......
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我們假設(shè)child
是具體的View
,而不是ViewGroup
区匠,View
的measure
方法最終調(diào)用了View
的onMeasure
方法干像,onMeasure
方法最終是測(cè)量View
的寬高;
我們回過(guò)頭來(lái)看:注釋15 的代碼最終會(huì)調(diào)用 View.onLayout
方法驰弄,它的調(diào)用過(guò)程是:
ViewRootImpl.performLayout
-> host.layout(ViewGroup.layout)
->super.layout(View.layout)
-> View.onLayout
麻汰,
最終完成View
位置的確定;
注釋16 的代碼最終會(huì)調(diào)用 View.onDraw
方法戚篙,它的調(diào)用過(guò)程是 :ViewRootImpl.performDraw
-> ViewRootImpl.draw
->ViewRootImpl.drawSoftware
-> mView.draw(DecorView.draw)
->super.draw(View.draw)
-> View.onDraw
五鲫,
最終完成 View 的繪畫(huà)出來(lái);
好了岔擂,現(xiàn)在我們可以解答上面剩下的疑問(wèn)了位喂,AcitivityThread.handleResumeActivity
方法 先調(diào)用自己的performResumeActivity
方法,而該方法最終調(diào)用Activity
的onResume
方法乱灵,而后再調(diào)用Activity
的makeVisible
方法塑崖,Activity
的makeVisible
方法最終完成View
的測(cè)量寬高、位置確定和繪畫(huà)痛倚,所以Activity
的onResume
方法不能直接獲取View
的寬规婆。
我們?cè)诨剡^(guò)頭來(lái)看,注釋19 的代碼蝉稳,它的延時(shí)時(shí)間為0啊抒蚜,而且比 View 的onMeasure
、onLayout
和onDraw
方法先被調(diào)用啊颠区,為什么從上面的demo
日志看出最終Runnable
接口等View
的onMeasure
削锰、onLayout
和onDraw
方法調(diào)用完之后再調(diào)用?
是因?yàn)樽⑨?0 的代碼先比注釋19 的代碼先被調(diào)用毕莱,注釋10 表示開(kāi)啟了同步消息屏障器贩,Android
中它有一個(gè)異步消息優(yōu)先級(jí)比較高的權(quán)利,保障 View 繪制完后再給其他消息執(zhí)行朋截,所以在 View.post
方法中的Runnable
接口的回調(diào)能直接獲取View
的寬蛹稍。
Activity
的onResume
方法是在Activity
的makeVisible
方法先被調(diào)用的,而View
的post
中Runnable
接口是在View
繪制完才會(huì)被回調(diào)的部服,所以Activity
的onResume
方法先比 View.post
方法中Runnable
接口被調(diào)用唆姐。
本文轉(zhuǎn)載于:https://mp.weixin.qq.com/s/8LnmMAc1NnClqHAZGYwQtg