一驯耻、解析Activity的構(gòu)成
1发皿、DecorView的創(chuàng)建
當(dāng)我們調(diào)用startActivity
方法時(shí)崔慧,最終調(diào)用ActivityThread#handleLaunchActivity
,該方法中會(huì)首先會(huì)調(diào)用Activity的onCreate
方法穴墅。在onCreate
方法中惶室,會(huì)調(diào)用Activity#setContentView
,setContentView
內(nèi)部會(huì)調(diào)用Activity的成員變量mWindow的(Window是抽象類玄货,其實(shí)現(xiàn)類是PhoneWindow皇钞,mWindow是PhoneWindow的一個(gè)實(shí)例)setContentView。其setContentView方法中松捉,首先new一個(gè)DecorView對(duì)象夹界,然后DecorView對(duì)象會(huì)根據(jù)不同的情況(主題,Window的feature等)加載不同的布局資源隘世。DecorView是Activity中的根View可柿,繼承了FrameLayout。至此DecorView創(chuàng)建完成丙者。
2复斥、添加DecorView到Window
完成DecorView的創(chuàng)建之后,接著調(diào)用ActivityThread#handleResumeActivity
方法械媒。在handleResumeActivity方法中目锭,首先調(diào)用Activity#onResume方法,handleResumeActivity方法接著會(huì)得到一個(gè)DecorView對(duì)象和一個(gè)WindowManager對(duì)象(接口滥沫,實(shí)現(xiàn)類是WindowManagerImpl)侣集,然后調(diào)用WindowManagerImpl#addView方法,DecorView對(duì)象作為入?yún)魅肜夹濉T赪indowManager#addView中世分,創(chuàng)建了一個(gè)ViewRootImpl對(duì)象(ViewRoot的實(shí)現(xiàn)類),并調(diào)用了ViewRootImpl#setView缀辩,DecorView對(duì)象作為入?yún)⒊袈瘛T赩iewRootImpl#setView方法內(nèi)部,會(huì)通過(guò)跨進(jìn)程的方式向WMS(WindowManagerService)發(fā)起一個(gè)調(diào)用臀玄,從而將DecorView最終添加到Window上瓢阴,才能真正顯示出來(lái)。在這個(gè)過(guò)程中健无,ViewRootImpl荣恐、DecorView和WMS會(huì)彼此關(guān)聯(lián),最后通過(guò)WMS調(diào)用ViewRootImpl#performTraverals方法開(kāi)始View的測(cè)量、布局叠穆、繪制流程少漆。
Window是一個(gè)抽象類,具體是實(shí)現(xiàn)是PhoneWindow硼被,Activity示损、Dialog等的視圖都需要附加到Window上來(lái)呈現(xiàn)。
WindowManager是外界訪問(wèn)Window的入口嚷硫,實(shí)現(xiàn)類是WindowManagerImpl检访,Window的具體實(shí)現(xiàn)是在WindowManagerService中,WindowManager和WindowManagerService的交互是一個(gè)IPC過(guò)程仔掸。脆贵。
DecorView是頂級(jí)View,是一個(gè)FrameLayout布局嘉汰,代表了整個(gè)應(yīng)用的界面丹禀。內(nèi)部有titlebar和contentParent兩個(gè)子元素,contentParent的id是content鞋怀,而我們?cè)O(shè)置的main.xml布局則是contentParent里面的一個(gè)子元素双泪。
ViewRoot的實(shí)現(xiàn)類是ViewRootImpl,在WindowManager中創(chuàng)建密似,用于將DecorView添加到Window中焙矛。
二、理解MeasureSpec
MeasureSpec代表一個(gè)32位int值残腌,高2位代表SpecMode(測(cè)量模式)村斟,低30位代表SpecSize(某種測(cè)量模式下的規(guī)格大小)。
//主要理解 & ~ | 位運(yùn)算的作用抛猫,體會(huì)這樣設(shè)計(jì)的妙處
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;//11000000 0000...000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec通過(guò)將SpecSize和SpecMode打包成了一個(gè)int值來(lái)避免過(guò)多對(duì)象的內(nèi)存分配蟆盹。
SpecMode有三類:
UNSPECIFIED :父容器不對(duì)View進(jìn)行任何限制,要多大給多大闺金,一般用于系統(tǒng)內(nèi)部逾滥。
EXACTLY:父容器檢測(cè)到View所需要的精確大小,這時(shí)候View的最終大小就是SpecSize所指定的值败匹,對(duì)應(yīng)LayoutParams中的match_parent和具體數(shù)值這兩種模式(也不一定寨昙,還受父容器影響,詳見(jiàn)下面的表格)掀亩。
AT_MOST:父容器指定了一個(gè)可用大小即SpecSize舔哪,View的大小不能大于這個(gè)值,對(duì)LayoutParams中的wrap_content槽棍。
說(shuō)明:上面描述的是理論上應(yīng)該有的邏輯捉蚤。
對(duì)于頂級(jí)DecorView抬驴,其MeasureSpec是由窗口尺寸和自身的LayoutParams共同確定。對(duì)于普通的View缆巧,其MeasureSpec由父容器和自身的LayoutParams共同確定怎爵。一旦MeasureSpec確定,onMeasure中就可以確定View的測(cè)量寬/高盅蝗。
三、View的工作流程
主要指measure姆蘸、layout墩莫、draw這三大流程。measure確定View的測(cè)量寬/高逞敷,layout確定View的最終寬/高和四個(gè)頂點(diǎn)的位置狂秦,而draw則將View繪制到屏幕上。
ViewRootImpl#performTraversals
會(huì)依次調(diào)用performMeasure
推捐、performLayout
和performDraw
三個(gè)方法裂问,這三個(gè)方法分別開(kāi)啟頂級(jí)View的measure、layout和draw這三大流程牛柒。
其中performMeasure
中會(huì)調(diào)用頂級(jí)View#measure 方法堪簿,measure調(diào)用onMeasure
,在onMeasure 方法中則會(huì)測(cè)量自身并調(diào)用所有子元素measure方法皮壁,這樣就完成了一次measure過(guò)程椭更;子元素會(huì)重復(fù)父容器的measure過(guò)程,如此反復(fù)完成了整個(gè)View樹(shù)的遍歷蛾魄。另外兩個(gè)過(guò)程同理虑瀑。
1、ViewGroup的Measure流程
對(duì)于ViewGroup既要測(cè)量自身滴须,也要遍歷子元素的measure方法(通過(guò)實(shí)現(xiàn)onMeasure方法)舌狗。
在performMeasure方法中,調(diào)用了DecorView#measure(繼承自View扔水,其實(shí)調(diào)用的是View#measure)痛侍,measure會(huì)調(diào)用onMeasure方法。ViewGroup并沒(méi)有定義onMeasure铭污,這個(gè)方法需要子類去實(shí)現(xiàn)恋日,主要需要實(shí)現(xiàn)兩個(gè)功能:①測(cè)量自身②測(cè)量子View。
ViewGroup提供了measureChildWithMargins
和measureChildren
方法嘹狞。
1.1岂膳、measureChildWithMargins方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//入?yún)ⅲ焊溉萜鞯腗easureSpec;父的padding和自身的margin(剩下為子元素可用空間)磅网;自身的寬度谈截。
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//注意:此時(shí)的入?yún)⑹亲陨淼腗easureSpec。measure又會(huì)調(diào)用child#onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
從上面的方法可以看出,View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定簸喂,MeasureSpec一旦確定毙死,onMeasure中就可以確定View的測(cè)量寬/高。getChildMeasureSpec(int spec, int padding, int childDimension)
方法的邏輯整理出如下表格:
表中的parentSize是指父容器目前可以使用的大小喻鳄,即父容器的specSize減去入?yún)adding扼倘。
ViewGroup并沒(méi)有定義onMeasure,需要其子類去實(shí)現(xiàn)除呵,為什么ViewGroup不像View一樣對(duì)其onMeasure做統(tǒng)一呢再菊?因?yàn)椴煌腣iewGroup子類有不同的布局特征,導(dǎo)致測(cè)量細(xì)節(jié)各不相同颜曾,無(wú)法統(tǒng)一纠拔。
根據(jù)上面的表格,我們發(fā)現(xiàn)父容器的MeasureSpec屬性為AT_MOST泛豪,子元素的LayoutParams為WRAP_CONTENT的時(shí)候稠诲,子元素的測(cè)量模式為AT_MOST,它的SpecSize為父容器的SpecSize減去padding(入?yún)ⅲ┕钍铮簿褪钦f(shuō)子元素WRAP_CONTENT和MATCH_PARENT一樣的臀叙。為了解決這個(gè)問(wèn)題,需要在WRAP_CONTENT時(shí)指定一下默認(rèn)的寬高岗仑。
1.2匹耕、measureChildren方法
measureChildren
中會(huì)循環(huán)調(diào)用measureChild方法,在measureChild中荠雕,首先會(huì)調(diào)用getChildMeasureSpec方法稳其,入?yún)⒑蜕厦骖愃疲瑓^(qū)別在于padding入?yún)H僅為自身的padding炸卑,然后會(huì)調(diào)用子元素的measure方法(和measureChildWithMargins
非常類似)既鞠。
2、View的Measure過(guò)程
View的measure方法是一個(gè)final方法盖文,會(huì)調(diào)用onMeasure方法嘱蛋,因此只需要關(guān)注onMeasure方法,入?yún)樽约旱膍easureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension
用于設(shè)置測(cè)量的寬高五续,測(cè)量好之后洒敏,必須調(diào)用。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
簡(jiǎn)單理解疙驾,getDefaultSize返回的就是measureSpec中的specSize凶伙,這就是View測(cè)量后的大小。在AT_MOST和EXACTLY模式下它碎,都返回了specSize函荣。也就是說(shuō)對(duì)于一個(gè)直接繼承View的自定義View显押,它的wrap_content和match_parent效果一樣,因此如果要實(shí)現(xiàn)自定義View的wrap_content傻挂,則要重寫onMeasure方法乘碑。解決問(wèn)題:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 MeasureSpec.AT_MOST 模式下,給定一個(gè)默認(rèn)值mWidth,mHeight金拒。默認(rèn)寬高靈活指定
//參考TextView兽肤、ImageView的處理方式
//其他情況下沿用系統(tǒng)測(cè)量規(guī)則即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
getSuggestedMinimumWidth()
方法就是:如果View沒(méi)有設(shè)置背景,就返回minWidth屬性值(可以為0)绪抛;如果設(shè)置了背景轿衔,就返回minWidth和背景的最小寬度之間的最大值。
View的measure過(guò)程是三大流程中最復(fù)雜的一個(gè)睦疫,measure完成以后,通過(guò) getMeasuredWidth/Height 方法就可以正確獲取到View的測(cè)量后寬/高鞭呕。在某些情況下蛤育,系統(tǒng)可能需要多次measure才能確定最終的測(cè)量寬/高,所以在onMeasure中拿到的寬/高很可能不是準(zhǔn)確的葫松。一個(gè)較好的習(xí)慣是在onLayout方法中瓦糕,去獲取View測(cè)量寬高或最終寬高。
3腋么、如何正確獲得寬高
如果我們想要在Activity啟動(dòng)的時(shí)候就獲取一個(gè)View的寬高咕娄,怎么操作呢?因?yàn)閂iew的measure過(guò)程和Activity的生命周期并不是同步執(zhí)行珊擂,無(wú)法保證在Activity的 onCreate圣勒、onStart、onResume 時(shí)某個(gè)View就已經(jīng)測(cè)量完畢摧扇。所以有以下四種方式來(lái)獲取View的寬高:
3.1圣贸、Activity/View#onWindowFocusChanged
onWindowFocusChanged這個(gè)方法的含義是:VieW已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了扛稽,需要注意:它會(huì)被調(diào)用多次吁峻,當(dāng)Activity的窗口得到焦點(diǎn)和失去焦點(diǎn)均會(huì)被調(diào)用。
3.2在张、view.post(runnable)
通過(guò)post將一個(gè)runnable投遞到消息隊(duì)列的尾部用含,當(dāng)Looper調(diào)用此runnable的時(shí)候,View也初始化好了帮匾。
3.3啄骇、ViewTreeObserver
使用 ViewTreeObserver 的眾多回調(diào)可以完成這個(gè)功能,比如OnGlobalLayoutListener 這個(gè)接口辟狈,當(dāng)View樹(shù)的狀態(tài)發(fā)送改變或View樹(shù)內(nèi)部的View的可見(jiàn)性發(fā)生改變時(shí)肠缔,onGlobalLayout 方法會(huì)被回調(diào)夏跷,這是獲取View寬高的好時(shí)機(jī)。需要注意的是明未,伴隨著View樹(shù)狀態(tài)的改變槽华, onGlobalLayout 會(huì)被回調(diào)多次。
3.4趟妥、view.measure(int widthMeasureSpec,intheightMeasureSpec)
手動(dòng)對(duì)view進(jìn)行measure猫态。需要根據(jù)View的layoutParams分情況處理:
- match_parent:直接放棄。根據(jù)上表所示披摄,需要知道parentSize亲雪,即父容器剩余空間,而此時(shí)無(wú)法知道這個(gè)值疚膊。
- 具體的數(shù)值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二進(jìn)制表示义辕,最大值30個(gè)1,在AT_MOST模式下寓盗,我們用View理論上能支持的最大值去構(gòu)造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
四灌砖、layout過(guò)程
layout
方法確定View本身的位置,會(huì)調(diào)用onLayout
方法傀蚌。onLayout
確定所有子元素的位置基显,通過(guò)遍歷所有的子View并調(diào)用其layout
方法。
View#layout中善炫,setFrame
確定View的四個(gè)頂點(diǎn)位置撩幽,即初始化mLeft,mRight箩艺,mTop窜醉,mBottom這四個(gè)值(確定了最終的寬高),也就確定了View在父容器中的位置艺谆。接著調(diào)用onLayout方法酱虎,確定所有子View的位置,和onMeasure一樣擂涛,onLayout的具體實(shí)現(xiàn)和布局有關(guān)读串,因此View和ViewGroup均沒(méi)有真正實(shí)現(xiàn)onLayout方法。
View的測(cè)量寬高和最終寬高的區(qū)別:
在View的默認(rèn)實(shí)現(xiàn)中撒妈,View的測(cè)量寬高和最終寬高相等恢暖,只不過(guò)測(cè)量寬高形成于measure過(guò)程,最終寬高形成于layout過(guò)程狰右。即便View需要多次測(cè)量才能確定自己的測(cè)量寬高杰捂,但最終來(lái)說(shuō),測(cè)量寬高和最終寬高還是一致棋蚌。
五嫁佳、draw過(guò)程
View的繪制過(guò)程遵循如下幾步:
- 繪制背景 drawBackground(canvas)
- 繪制自己 onDraw
- 繪制children dispatchDraw 遍歷所有子View的 draw 方法
- 繪制裝飾 onDrawScrollBars
View#setWillNotDraw挨队,如果一個(gè)View不需要繪制任何內(nèi)容,那么置為ture蒿往,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化盛垦。默認(rèn)情況下,View為false瓤漏,ViewGroup為true腾夯。所以自定義ViewGroup需要通過(guò)onDraw來(lái)繪制內(nèi)容時(shí),必須顯式的關(guān)閉 WILL_NOT_DRAW 這個(gè)優(yōu)化標(biāo)記位蔬充,即調(diào)用 setWillNotDraw(false)蝶俱。