Android View的繪制流程分析

1 Android UI 框架的核心類

在梳理View的繪制流程之前,我們先來熟悉一下AndroidUI框架中幾個重要類的概念:

Window: 外觀和行為策略的抽象基類,它的實例應(yīng)該作為頂級視圖被添加到 Window Manager 中。它提供標準的UI策略,例如 background,標題區(qū)域,默認關(guān)鍵處理等。

PhoneWindow: 是 Window 的唯一實現(xiàn)類,在 Activity 的 attach() 方法中構(gòu)造一個 PhoneWindow 實例。

DecorView: 在 PhoneWindow 中創(chuàng)建的頂級視圖,是 View Tree 的實際根節(jié)點,由此可見,View 是依附在 Window 之上的。

ViewRootImpl:?視圖層次結(jié)構(gòu)的頂部,在 View(DecorView) 和 WindowManager 之間實現(xiàn)所需的協(xié)議。 由 WindowMangerGlobal 的 addView() 方法創(chuàng)建。

WindowManagerImpl: 實現(xiàn)了 WindowManager 接口,是整個應(yīng)用程序中所有 Window 的管理者。在 Activity 的 attach() 方法中由 PhoneWindow 調(diào)用 setWindowManager() 方法創(chuàng)建。

WindowManagerGlobal: 在 WindowManagerImpl 中創(chuàng)建的單例對象,在整個 Android 進程中是一個全局變量,它包含了能和 WMS 進行雙向 Binder 通信的通道。一個應(yīng)用進程中所有的 Activity 都是通過這個進程內(nèi)唯一的?WindowManagerGlobal 對象和 WMS 進行通信。

WindowManagerService: 遠程窗口管理類,從它的角度,并不關(guān)心應(yīng)用中的各種View,它管理和調(diào)度的對象是Window。

WindowManager 類關(guān)系圖

總結(jié):

在 Activity 的 attach() 方法中,會創(chuàng)建 PhoneWindow 和 WindowMangerImpl 實例。而 WindowMangerImpl 持有?WindowManagerGlobal 的引用。

在 Activity 的 onCreate() 回調(diào)方法中,setContentView() 方法會調(diào)用 PhoneWindow#setContentView() 方法,如果 DecorView 對象還未創(chuàng)建,會調(diào)用 installDecor() 方法來創(chuàng)建 Activity 中的 DecorView。并將 inflate 出的 View 添加到 DecorView的 ContentView 中,而 ContentView 繼承自 FrameLayout。

在 Activity 的 makeVisible() 方法中,調(diào)用了 WindowManager (WindowManagerGlobal) 的 addView() 方法,參數(shù)是 DecorView 對象,在addView() 方法中最重要的是創(chuàng)建了ViewRootImpl 對象,并通過 setView() 方法把 ViewRootImpl 對象和 DecorView 對象關(guān)聯(lián)在一起。

WindowManagerService 通過與 WindowManagerGlobal 的雙向 Binder 通信完成對 Window (PhoneWindow) 的管理。

WindowManagerGlobal 通過 ViewRootImpl 完成對 Window (PhoneWindow)?和 View (DecorView) 的管理。

2 View 的繪制流程分析

下面開始對View的繪制流程進行梳理:

首先,View Tree 發(fā)生重繪基本都會跟 View # requestLayout() 或 View # invalidate() 方法的調(diào)用相關(guān)。

那么我們首先來看一下 View 中的 requestLayout() 方法的源碼

View # requestLayout()

可以看到,16行,在 mPrivateFlags 中添加了 PFLAG_FORCE_LAYOUT 標記,該標記將在之后的 measure 流程中標記哪些View 需要重新進行尺寸的測量。第20行,調(diào)用了父View 中的 requestLayout() 方法,該方法會一直向上傳遞,直到父View 是 ViewRootImpl,最終調(diào)用 ViewRootImpl # requestLayout() 方法,開始一次 View Tree 遍歷。

那么我們來看一下 ViewRootImpl 中 requestLayout() 中的源碼

ViewRootImpl # requestLayout()

在 RootViewImpl 類中,requestLayout() 和 invalidate() 方法都會調(diào)用 scheduleTraversals(),這是一個異步方法,最終會調(diào)用到 performTraversals() 方法,它是 View 繪制流程的核心方法,分別調(diào)用 performMeasure()、performLayout()、performDraw() 方法來進行 View 的尺寸測量 (measure),位置測量 (layout),繪制 (draw) 三大流程。

RootViewImpl #?performTraversals()

2.1 measure 測量 View 的大小

在 ViewRootImpl # performTraversals() 中我們看到調(diào)用了 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 方法,參數(shù)?childWidthMeasureSpec 和?childHeightMeasureSpec 是 MeasureSpec 類型,分別表示了該 ViewRootImpl 關(guān)聯(lián)的 頂層 View 對象的尺寸。注意,該頂層 View 在Activity 中是 DecorView,但是若只是通過 WindowManger # addView添加的懸浮框則不是。(MeasureSpec 的概念不再贅述)

若頂層 View 是 DecorView 對象,DecorView 繼承自 FrameLayout,而 ViewGroup 繼承自 View 類,且 View 的 measure() 方法是 final 修飾的,并不能被子類重寫,因此我們來一起看一下這個 View # measure() 方法。

View # measure

可以看到,第4行中,使用到了起初 View # requstLayout() 中進行標記的?PFLAG_FORCE_LAYOUT,?用以表明該 View 是否需要被重新測量(measure),在27行,調(diào)用了onMeasure 方法,由于 DecorView 是 FrameLayout 子類,因此它實際上調(diào)用的是?DecorView # onMeasure()?方法。37行,mPrivateFlags 添加了?PFLAG_LAYOUT_REQUIRED 標記,表示該 View 需要被重新布局(layout)。下面我們來看一下 FrameLayout # onMeasure() 方法。

FrameLayout # onMeasure()

FrameLayout # onMeasure() 方法, 20行,調(diào)用?measureChildWithMargins() 對子 View 進行測量,該方法需要除去父容器的 padding 和 margin之后計算子 View 的大小,這個過程會遞歸測量所有的子View 直到達到葉子節(jié)點。maxWidth 和 maxHeight 記錄了子類中的最大寬度和最大高度,并添加 padding ,并把測量大小賦值給該 FrameLayout。

概括一下整個流程:測量始于DecorView,通過不斷的遍歷子View的measure方法,根據(jù)ViewGroup的MeasureSpec及子View的LayoutParams來決定子View的MeasureSpec,進一步獲取子View的測量寬高,然后逐層返回,不斷保存ViewGroup的測量寬高。

2.2 layout 計算 View 的位置

對于DecorView來說,調(diào)用layout方法,就是對它自身進行布局,注意到傳遞的參數(shù)分別是0, 0, host.getMeasuredWidth, host.getMeasuredHeight,它們分別代表了一個View的上下左右四個位置,顯然,DecorView的左上位置為0,然后寬高為它的測量寬高。

View # layout()

在13行,setOpticalFrame(l, t, r, b) 和 setFrame(l, t, r ,b) 都是對 View 的左上角和右下角進行賦值,而在15行,當 changed = true 表示View 的位置發(fā)生了變化,或者 PFLAG_LAYOUT_REQUIRED 標記滿足時,會調(diào)用 View 的 onLayout() 方法,若該方法在 ViewGroup 中調(diào)用,用于確定子 View 的位置,即在該方法內(nèi)部,子View會調(diào)用自身的 layout 方法來進一步完成自身的布局流程。由于不同的布局容器的onMeasure方法均有不同的實現(xiàn),現(xiàn)在仍然以 FrameLayout # onLayout 方法進行講解。

FrameLayout # onLayout() 和 layoutChildren()
FrameLayout # onLayout() 和 layoutChildren()

在 FrameLayout 的 onLayout() 方法中,不需要考慮其他常用布局如 LinearLayout,GridLayout 等有各子對象競爭布局空間的問題,F(xiàn)rameLayout 中的各子View 是“層疊”而成的,只需要考慮到 Gravity 屬性來確定子View 相對于父布局的位置。

第5行,在 onLayout 方法內(nèi)部直接調(diào)用了layoutChildren方法

先梳理一下以上邏輯:首先先獲取父容器的 padding 值,然后遍歷其每一個子 View,根據(jù)子View 的 layout_gravity 屬性、子View的測量寬高、父容器的 padding 值、來確定子View的布局參數(shù),然后調(diào)用 child.layout 方法,把布局流程從父容器傳遞到子元素。

那么,現(xiàn)在就分析完了ViewGroup的布局流程,那么我們接著分析子元素的布局流程。

子View的布局流程

子View的布局流程也很簡單,如果子View 是一個 ViewGroup,那么就會重復以上步驟,如果是一個View,那么會直接調(diào)用 View # layout 方法,根據(jù)以上分析,在該方法內(nèi)部會設(shè)置 view 的四個布局參數(shù),接著調(diào)用onLayout方法,我們看看?View # onLayout方法:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

2.3 draw 繪制 View 的內(nèi)容

一個 View 的 layout 確定之后,才能在此基礎(chǔ)上執(zhí)行 draw 流程。

由于 ViewGroup 沒有重寫 draw 方法,因此所有的 View 都是調(diào)用?View#draw?方法,因此,我們直接看它的源碼

View # draw()

可以看到,draw過程比較復雜,但是邏輯十分清晰,而官方注釋也清楚地說明了每一步的做法。我們首先來看一開始的標記位dirtyOpaque,該標記位的作用是判斷當前View是否是透明的,如果View是透明的,那么根據(jù)下面的邏輯可以看出,將不會執(zhí)行一些步驟,比如繪制背景、繪制內(nèi)容等。這樣很容易理解,因為一個View既然是透明的,那就沒必要繪制它了。接著是繪制流程的六個步驟,這里先小結(jié)這六個步驟分別是什么,然后再展開來講。

繪制流程的六個步驟:?

1、對View的背景進行繪制?

2、保存當前的圖層信息(可跳過)?

3、繪制View的內(nèi)容?

4、對View的子View進行繪制(如果有子View)?

5、繪制View的褪色的邊緣,類似于陰影效果(可跳過)?

6、繪制View的裝飾(例如:滾動條)?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容