最近重新看了一下任玉剛大佬的《Android 開發(fā)藝術(shù)探索》,寫了篇筆記,分享給大家。
1. ViewRootImpl 與 DecorView

接下來的講解的源碼版本為 Android 10 。
ViewRootImpl 是連接 WindowManager 和 DecorView 的紐帶,測量、放置和繪制三大流程都是通過 ViewRootImpl 實(shí)現(xiàn)的。
在 ActivityThread 的 handleResumeActivity() 方法中,會調(diào)用 WindowManager 的 addView() 方法,而具體添加 DecorView 的操作是在 WindowManagerGlobal 中。
在 WindowManagerGlobal 的 addView() 方法中,會把 DecorView 添加到 Window 中,同時(shí)會創(chuàng)建 ViewRootImpl ,并調(diào)用 ViewRootImpl 的 setView() 方法 把 ViewRootImpl 和 DecorView 關(guān)聯(lián)起來。
View 的繪制流程是從 ViewRootImpl 的 performTraversals() 方法開始的,它經(jīng)過測量(measure)、放置(layout)和繪制(draw)三個(gè)過程才能把一個(gè) View 繪制出來,measure() 方法用于測量 View 的寬高,layout() 用于確定 View 在父容器中的放置位置,draw() 負(fù)責(zé)做具體的繪制操作。
針對 performTraversals 的大致流程,可用下圖表示。

View 繪制主要的三個(gè)方法就是 onMeasure()、 onLayout()、onDraw(),這三個(gè)方法要解決的問題就是畫多大、在哪畫、畫什么。
ViewRootImpl 的 performTraversal() 方法會依次調(diào)用 performMeasure()、performLayout() 和 performDraw() 三個(gè)方法,這三個(gè)方法分別完成 DecorView 的測量、放置和繪制三大流程。
performMeasure() 方法會調(diào)用 DecorView 的 measure() 方法,在 measure() 方法中又會調(diào)用自己的 onMeasure() 方法。
DecorView 的 onMeasure() 方法會調(diào)用父類 FrameLayout 的 onMeasure() 方法,在 FrameLayout 的 onMeasure() 方法中,會調(diào)用子元素的 onMeasure() 方法測量子元素的寬高,接著子元素會重復(fù)父容器的 measure 過程,如此反復(fù)完成整個(gè) View 樹的遍歷。
而 performLayout() 和 performDraw() 的執(zhí)行流程與 performMeasure() 是類似的。
measure 過程決定了 View 的寬高,layout 過程決定了 View 的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的 View 寬高,draw 過程則決定了 View 的具體繪制操作,只有 draw() 方法完成后 View 的內(nèi)容才會在屏幕上展示。
1.1 Activity 視圖層級結(jié)構(gòu)
假如我們有一個(gè)繼承了 AppCompatActivity 的 MainActivity,并且 activity_main 布局的內(nèi)容如下。

我們現(xiàn)在能感知到的視圖層級是下面這樣的。

當(dāng)我們在 MainActivity 中調(diào)用父類的 setContentView() 后,AppCompatActivity 會調(diào)用 AppCompatDelegateImpl 的 setContentView() 方法,AppCompatDelegateImpl 在這個(gè)方法中會把 RelativeLayout 添加到 id 為 content 的 ViewGroup 中。

其中 ContentFrameLayout 也就是 id 為 content 的 ViewGroup 。

ensureSubDecor() 方法會在 subDecor 沒有初始化時(shí)用 createSubDecor() 方法創(chuàng)建 subDecor ,createSubDecor() 方法會調(diào)用 Window 的 setContetnView() 方法,把 abc_screen_toolbar 布局設(shè)為 Window 的內(nèi)容視圖,而這里的 mHasActionBar 只有在 feature 為 FEATURE_SUPPORT_ACTION_BAR 時(shí)才會為 true。
abc_screen_toolbar 布局的內(nèi)容如下。

把 RelativeLayout 放到 mSubDecor 中后,視圖層級就變成下面這樣了。

Window 的實(shí)現(xiàn)類為 PhoneWindow,在 PhoneWindow 的 setContentView() 方法中,會調(diào)用 installDecor() 方法創(chuàng)建 DecorView ,然后調(diào)用 LayoutInflate 的 inflate() 方法把 ActionBarOverlayLayout 加入到 DecorView 中。

在 installDecor() 方法中,會調(diào)用 generateLayout() 方法生成 mContentParent。
在 generateLayout() 方法中,會根據(jù)不同的 feature 來生成不同的 DecorView,比如沒有設(shè)定任何 feature 時(shí),對應(yīng)的 DecorView 的布局就是 screen_simple 。
screen_simple 布局的實(shí)現(xiàn)如下。

前面的布局加入到 screen_simple 中后,視圖層級就是下面這樣的。

這里的 action_mode_bar_stub 是用來顯示 ActionMode 的,而 FrameLayout 就是 ID_ANDROID_CONTENT 對應(yīng)的 ViewGroup。
到這里好像還是少了點(diǎn)什么,狀態(tài)欄哪去了?

根據(jù) Layout Inspector 的分析,LinearLayout 下面還有一個(gè) id 為 statusBarBackground 的 View ,根據(jù)這個(gè) id 在 DecorView 中找到了對應(yīng)的 mStatusColorViewState 。

而在 DecorView 的 updateColorViewInt() 方法中,則把狀態(tài)欄通過 addView() 方法添加到了 DecorView 中。
該方法的調(diào)用時(shí)序圖如下。

也就是完整的 DecorView 視圖層次如下。

上圖對應(yīng)的 View 樹如下。

2. 測量規(guī)格 MeasureSpec
按注釋來說,MeasureSpec 封裝了從父 View 傳給子 View 的布局要求,MeasureSpec 在很大程度上決定了一個(gè) View 的尺寸規(guī)格,具體的尺寸會受到父容器的影響,因?yàn)楦溉萜饔绊?View 的 MeasureSpec 的創(chuàng)建過程。
在測量過程中,系統(tǒng)會把 View 的 LayoutParams 根據(jù)父容器設(shè)定的規(guī)則轉(zhuǎn)換為對應(yīng)的 MeasureSpec,然后再根據(jù)這個(gè) MeasureSpec 測量出 View 的寬高。
要注意的是,這里的說的寬高是測量寬高,不一定是 View 的最終寬高,原因后面會講到。
MeasureSpec 代表一個(gè) 32 位 int 值,高 2 位代表測量模式 SpecMode,低 30 位代表規(guī)格大小 SpecSize,MeasureSpec 通過把 SpecMode 和 SpecSize 打包成一個(gè) int 值避免過多的對象內(nèi)存分配,
MeasureSpec 中定義了下面三種測量規(guī)格。

-
待定 UNSPECIFIED
表示父 View 對子 View 的大小不做限制;
-
精確 EXATCTLY
父 View 計(jì)算好了子 View 具體的寬高,子 View 的最終大小就是 SpecSize 指定的值;
-
最多 AT_MOST
父 View 指定了一個(gè)可用大小,View 的大小不能大于這個(gè)值;
MeasureSpec 用來打包 SpecMode 和 SpecSize 的方法是 makeMeasureSpec() ,代碼如下。

3. MeasureSpec 與 LayoutParams 的關(guān)系
在 View 測量時(shí),系統(tǒng)會把 LayoutParams 在父 View 的約束下,轉(zhuǎn)換成對應(yīng)的 MeasureSpec,然后再根據(jù)這個(gè) MeasureSpec 確定 View 測量后的寬高,要靠 LayoutParams 和父 View 一起才能決定子 View 的 測量模式。
DecorView 的測量規(guī)格由窗口的尺寸和其 LayoutParams 共同確定,而普通 View 的測量規(guī)格由父 View 的 MeasureSpec 和自身的 LayoutParams 決定,MeasureSpec 確定后,就可以在 onMeasure() 方法中確定 View 的測量寬高。
在 ViewRootImpl 的 performTraversals() 方法中,有一段調(diào)用 measureHierarchy() 方法的代碼,也就是傳給 measureHierarchy() 的大小為屏幕尺寸。

measureHierarchy() 方法是用來設(shè)定子 View ,也就是 DecorView 的大小的。

measureHierarchy() 中的的 childWidthMeasureSpec 和 childHeightMeasureSpec 就是 DecorView 的測量規(guī)格 MeasureSpec。

通過上面代碼可以看出,DecorView 會根據(jù) LayoutParams 中的寬高來設(shè)定寬高測量規(guī)格。
-
MATCH_PARENT
精確模式,DecorView 大小就是窗口大??;
-
WRAP_CONTENT
最大模式,大小不定,但是不能超過窗口大??;
-
固定大小
精確模式,大小為 LayoutParams 中指定的大?。?/p>
對于普通 View 來說,View 的 measure 過程由 ViewGroup 傳遞而來,而 ViewGroup 是在 measureChildWithMargins() 方法中確定子 View 的測量規(guī)格的。

下面是 ViewGroup 的 getChildMeasureSpec() 方法獲取子 View 的測量規(guī)格的方式。

其中一段代碼如下。

上面這段代碼中的 size 是去掉了 padding 后的 size。
這里要注意的是,不是所有 ViewGroup 都會用這樣的方式?jīng)Q定子 View 的測量規(guī)格,比如 RelativeLayout 用的就是不一樣的測量規(guī)格。
4. View 測量過程
對于 ViewGroup,除了要完成自己的測量,還要遍歷調(diào)用子元素的 measure() 方法,而 View 只需要通過 measure() 方法就能確定測量規(guī)格。
View 的測量過程由 View 的 measure() 方法完成,measure() 方法是一個(gè) final 類型的方法,子類不能重寫。
View 的 measure() 方法會調(diào)用 onMeasure() 方法,這個(gè)方法我們是可以重寫的,onMeasure() 的實(shí)現(xiàn)如下。

widthMeasureSpec 和 heightMeasureSpec 是從父 View 傳過來的寬高測量規(guī)格,getDefaultSize() 方法是用來獲取默認(rèn)寬高的,getDefaultSize() 的實(shí)現(xiàn)如下。

從 getDefaultSize() 方法中可以看出,當(dāng)測量模式為 UNSPECIFIED 時(shí),寬/高就是最小寬/高,當(dāng)測量模式為 AT_MOST 或 EXACTLY 時(shí),寬/高就是 ViewGroup 指定的 SpecSize。
View 的寬/高由 specSize 決定,直接繼承 View 的自定義控件需要重寫 onMeasure() 方法并設(shè)置 wrap_content 時(shí)的自身大小,否則咋布局中使用 wrap_content 相當(dāng)于使用 match_parent 。
從前面的代碼可以了解到,如果 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,這時(shí)它的寬/高為 specSize ,這時(shí) View 的 specSize 為 ViewGroup 的 specSize。
比如 activity_main 的布局是下面這樣的。

那么 MyView 測量后的大小就是 600 ,這個(gè) 600 是 dp 換算為 px 后的值。

ViewGroup 的 SpecSize 是自身剩余的空間大小,也就是默認(rèn)子 View 的寬/高為父 View 的剩余控件大小,相當(dāng)于為寬/高設(shè)定的 wrap_content 無效,變成了 match_parent 。
如果我們不想讓自定義 View 在寬/高設(shè)為 wrap_content 時(shí)與父 View 的大小一致,那我們可以像下面這樣設(shè)定自己的計(jì)算好的默認(rèn)寬/高。

下面來看下 ViewGroup 的測量過程。
ViewGroup 是一個(gè)抽象類,沒有定義測量的的具體過程,具體的測量過程需要子類實(shí)現(xiàn),下面以 LinearLayout 為例,看一下它的 onMeasure() 方法的實(shí)現(xiàn)。

LinearLayout 會根據(jù)我們設(shè)定的方向設(shè)定子 View 的測量規(guī)格,下面來看下 measureVertical() 的實(shí)現(xiàn)。

在 measureVertical() 方法中,把每一個(gè)子元素都傳給了 measureChildBeforeLayout() ,而 measureChildBeforeLayout() 只是調(diào)用了 ViewGroup 的 measureChildWithMargin() 方法。

5. View 放置過程
layout() 方法的作用是 ViewGroup 用于確定子元素的位置,當(dāng) ViewGroup 的位置確定后,會在 onLayout() 方法中遍歷所有子 View 并調(diào)用子 View 的 layout() 方法。
layout() 方法用于確定 View 自己的位置,而 onLayout() 方法則用于確定所有子元素的位置,View 的 layout() 方法的實(shí)現(xiàn)如下。

View 的 layout() 方法首先會通過 setFrame() 方法設(shè)定 View 的邊框,也就是 mLeft、mRight、mTop 和 mBottom 四個(gè)頂點(diǎn)的值,這時(shí) View 在父 View 中的位置就確定了。

設(shè)定了四個(gè)頂點(diǎn)后,layout() 方法就會調(diào)用 onLayout() 方法確定子 View 的位置,View 和 ViewGroup 都沒有實(shí)現(xiàn) onLayout() 方法,下面以 LinearLayout 為例,看下 LinearLayout 的 onLayout() 方法的實(shí)現(xiàn)。
LinearLayout 的 onLayout() 方法會根據(jù)不同的排列方向調(diào)用不同的放置方法,當(dāng)方向?yàn)?VERTICAL 時(shí),對應(yīng)的放置方法為 layoutVertical() ,下面來看下 layoutVertical() 方法的實(shí)現(xiàn)。

layoutVertical() 方法會遍歷所有子 View 并調(diào)用 setChildFrame() 方法指定子 View 的邊框(frame)在哪個(gè)位置,而 setChildFrame() 方法只是簡單調(diào)用了 子 View 的 layout() 方法。
childTop 的值會逐漸增加,下一個(gè)子 View 的 top 為上一個(gè)子 View 的 bottom,也就是排列方向?yàn)?VERTICAL 的 LinearLayout 的特性。
6. View 繪制過程
View 繪制分為下面 6 步:
- 繪制背景
- 保存 Canvas 圖層為后續(xù)淡出做準(zhǔn)備(可選)
- 繪制 View 的內(nèi)容
- 繪制子 View (dispatchDraw)
- 繪制淡出邊緣并恢復(fù) Canvas 圖層(可選)
- 繪制裝飾(比如 foreground 和 scrollbar)
一般情況下第 2 步和第 5 步是不執(zhí)行的。

下面來看下繪制相關(guān)方法的實(shí)現(xiàn)。

drawBackground() 方法首先會通過 Drawable 的 setBounds() 方法設(shè)置背景繪制的范圍,然后如果我們調(diào)用過 scrollTo() 方法,那么 drawBackground() 就會把畫布平移到指定位置后再繪制。
View 和 ViewGroup 沒有實(shí)現(xiàn) onDraw() 方法,接下來就是 dispatchDraw() 方法,View 沒有實(shí)現(xiàn)這個(gè)方法,下面來看下 ViewGroup 的 dispatchDraw() 方法的實(shí)現(xiàn)。

在 ViewGroup 的 dispatchDraw() 方法中,首先會調(diào)用 buildOrderedChildList() 方法獲取子 View 列表,然后遍歷子 View ,通過 drawChild() 方法調(diào)用每一個(gè)子 View 的 draw() 方法。
而第 6 步 drawForegounrd() 只是獲取 foreground 對應(yīng)的 Drawable 并調(diào)用它的 draw() 方法。
7. 總結(jié)
根據(jù)前面講解的內(nèi)容,從 ViewRootImpl 的 performTraversals() 方法開始,大致的方法調(diào)用時(shí)序圖如下。

View 繪制的三大過程分別是測量、放置和繪制,對應(yīng)的的三個(gè)方法為 onMeasure() 、onLayout() 和 onDraw() 。
測量過程中最重要的就是理解 MeasureSpec 以及自定義 View 時(shí)要重寫 onMeasure() 方法設(shè)置默認(rèn)寬高。
MeasureSpec 由測量模式 SpecMode 和 SpecSize 組成,SpecMode 分為待定(UNSPECIFIED)、精確(EXACTLY)和最大(AT_MOST)。
放置過程中最關(guān)鍵的方法就是 setFrame() ,這個(gè)方法會把父 View 在 onLayout() 方法中計(jì)算好的四個(gè)頂點(diǎn)的值賦值給 mTop、mLeft 、mRight 和 mBottom 。
繪制過程的 draw() 方法中主要的 4 個(gè)繪制步驟為:繪制背景、繪制 View 內(nèi)容、繪制子 View 內(nèi)容以及繪制裝飾。