天空看不見云准潭,大火球在上面肆意發(fā)光,逼著毛孔慢慢滲出汗水域仇。
我離開舒適區(qū)惋鹅,跑出去面試了幾次。
得到的最多的反饋是不夠深入殉簸。
作為一個五年經(jīng)驗的安卓開發(fā)者闰集,欠缺的還有很多。
前言
從一個view實例被創(chuàng)建般卑,到展示到屏幕上武鲁,都經(jīng)歷了怎么樣的一個流程?在安卓開發(fā)中蝠检,這似乎是一個基本的知識沐鼠,應(yīng)該被開發(fā)者清楚地認(rèn)識明白,面試中也作為問題頻頻出現(xiàn),然而我還是認(rèn)識得不深刻饲梭。
Android View
的繪制流程 是View相關(guān)的核心知識點乘盖。我希望通過這篇文章學(xué)習(xí)并分享Android View繪制流程的始末。
并將其刻在腦子里憔涉。
目錄
本文分為以下流程學(xué)習(xí)订框,閱讀完本文將會學(xué)習(xí)到PhoneWindow
,WindowManger
,ViewRootImpl
,View
等關(guān)鍵類的聯(lián)系和作用。對window窗體機制以及繪制流程有所了解兜叨。
- 流程圖分析
- 了解view繪制流程
- 了解
setContentView
如何附加到內(nèi)容到頁面
關(guān)鍵類解釋
-
Choreographer
:協(xié)調(diào)動畫穿扳、輸入和繪圖的時間。Choreographer
從顯示子系統(tǒng)接收定時脈沖(例如垂直同步)国旷,然后安排工作發(fā)生矛物,作為渲染下一個顯示幀的一部分。
一. 流程圖分析
1.1 創(chuàng)建Activity到setContentView的窗口附加流程圖
下圖展示了window的創(chuàng)建到setContentView
之后的窗體view樹變化情況
1.2 view繪制流程圖
二. view繪制流程
2.1 繪制流程分析
在我們調(diào)用requestLayout
和 invalidate
的時候跪但,我們會讓view刷新布局和繪制履羞。所以從這兩個方法入手,可以完整地走一遍繪制流程屡久。
繪制動畫等行為主要通過Choreographer
類協(xié)調(diào)忆首。
- 調(diào)用
requestLayout
和invalidate
標(biāo)記繪制和充布局信息 -
Choreographer
接受系統(tǒng)垂直同步等脈沖消息,在scheduleTraversals
方法中回調(diào)執(zhí)行doTraversal
開始遍歷view樹涂身。 - 觸發(fā)
ViewRootImpl#performTraversals
完成view樹遍歷- 如果
layoutRequested
為true,measureHierarchy
中測量mView
及其子view
- 需要的話,觸發(fā)
ViewRootImpl#performLayout
完成布局 - 如果
view
沒有隱藏且TreeObserver
中沒有攔截繪制搓蚪,就調(diào)用performDraw
蛤售,完成繪制- 計算dirty臟區(qū)域
- 從mSurface中 獲取臟區(qū)域的canvas,交給view繪制
- 如果
2.2 ViewRootImpl
創(chuàng)建時機
從上面可以看到妒潭,所有的繪制和布局都是由ViewRootImpl#doTraversal
觸發(fā)悴能,然后對其持有的view樹進(jìn)行遍歷繪制。所以一定要了解ViewRootImpl
和其持有的DecorView
的創(chuàng)建和關(guān)聯(lián)時機雳灾。關(guān)鍵流程如下:
-
Activity#handleResume
的時候漠酿,調(diào)用WIndowManager#addView
添加decorView
- 調(diào)用到
WindowManagerGlobal#addView
的時候創(chuàng)建ViewRootImpl
實例。 - 調(diào)用
ViewRootImpl#setView
完成一系列初始化方法- 注冊
mDisplayListener
到DisplayManager
谎亩,接收顯示更新回調(diào) - 調(diào)用
requestLayout
更新一次布局大小和位置信炒嘲,以確保從系統(tǒng)接收任何其他事件之前進(jìn)行過一次布局 - 通過
WindowSession
調(diào)用addToDisplayAsUser,添加window
- 注冊
- 在接收系統(tǒng)事件的時候匈庭,調(diào)用scheduleTraversals 繪制view樹
WindowMangerGlobal 最終調(diào)用的其實都是ViewRootImpl方法夫凸。ViewRootImpl在addView關(guān)聯(lián)號DecorView后,還調(diào)用了setView方法進(jìn)行初始化阱持,接收垂直同步脈沖信息夭拌,代碼如下:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
...
// 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.
requestLayout();
...
try{
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
}
}
在初始化的最后,通過WindowSession
調(diào)用addToDisplayAsUser
添加了window
到屏幕顯示中。
三. 附加contentView到界面
當(dāng)我們啟動activity鸽扁,將我們寫的xml布局文件顯示在屏幕上蒜绽,其中經(jīng)歷了那些過程呢?我們要在界面上展示內(nèi)容桶现,有如下幾個步驟:
- 啟動activity躲雅,在
performLaunchActivity
的時候創(chuàng)建Activity
并且attach和調(diào)用onCreate方法 - 在attach的時候,創(chuàng)建PhoneWindow實例并持有mWindow引用
- 調(diào)用
setContentView
以附加內(nèi)容到windows中 - 通過確認(rèn)
decorView
以及subDecorView
存在巩那,創(chuàng)建DecorView
和subDecorView
- 添加
ContentView
到decorView
樹中的R.id.content
節(jié)點 - 當(dāng)
handleResumeActivity
的時候吏夯,調(diào)用WindowManager.addView
。關(guān)聯(lián)View
和ViewRootImpl
即横,后續(xù)便可以繪制噪生。
3.1 創(chuàng)建PhoneWindow
我們先看啟動activity的方法,ActivityThread#performLaunchAcivity
东囚。 從該方法源碼中可知跺嗽,啟動activity的方法流程如下:
- 創(chuàng)建Activity實例 ,在
Instrumentation#newActivity
完成 - 創(chuàng)建PhoneWindows附加到Activity页藻。在
Activity#attachAcitivity
完成 - 調(diào)用Activity的onCreate生命周期,代碼是
Instrumentation#callActivityOnCreate
- 在
onCreate
中執(zhí)行用戶自定義的代碼桨嫁,比如setContentView
。
所以可知份帐,在activity
準(zhǔn)備啟動的時候璃吧,就已經(jīng)完成了PhoneWindows
實例的創(chuàng)建。而接下來就執(zhí)行到了我們在Activity#onCreate
中調(diào)用setContentView
方法設(shè)置的自定義布局废境。
3.2 setContentView的本質(zhì)
activity
在啟動之后畜挨,我們通常在onCreate
調(diào)用setContentView
中設(shè)置自己的布局文件。我們來具體看看setContentView
做了什么噩凹。
setContentView
方法本質(zhì)其實是向android.R.id.content
添加自己巴元。
我們看AppCompatDelegateImpl#setContentView
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
///確認(rèn)好 window decorView 以及 subDecorView
ensureSubDecor();
//向 android.R.id.content 添contentView
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
這一塊代碼關(guān)鍵在于向id為android.R.id.content
的子view中添加contentView
。
addView
的過程自然會觸發(fā)布局的重新渲染驮宴。
關(guān)鍵之處還是在于ensureSubDecor()
方法中對于decoView
以及subDecorView
的實例化創(chuàng)建工作逮刨。
3.3 確認(rèn)window ,decorView 以及 subDecorView
先看看AppCompatDelegateImpl#ensureSubDecor()
的主要實現(xiàn):
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
}
private ViewGroup createSubDecor() {
// Now let's make sure that the Window has installed its decor by retrieving it
ensureWindow();
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//省略其他樣式subDecor布局的實例化
//包含 actionBar floatTitle ActionMode等樣式
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
//省略狀態(tài)欄適配代碼
//省略actionBar布局替換代碼
mWindow.setContentView(subDecor);
return subDecor;
}
代碼很長堵泽,上面是經(jīng)過省略之后的主要代碼修己。可以看到代碼邏輯很清晰:
- 步驟一:確認(rèn)window并attach(設(shè)置背景等操作)
- 步驟二:獲取DecorView迎罗,因為是第一次調(diào)用所以會installDecor(創(chuàng)建DecorView和Window#ContentLyout)
- 步驟三:從xml中實例化出subDecor布局
- 步驟四:設(shè)置內(nèi)容布局:
mWindow.setContentView(subDecor);
3.4 初始化 installDecor
關(guān)鍵兩處代碼是Window#installDecor
和 Window#setContentView
箩退。
先看一下Window#installDecor
的代碼:
private void installDecor() {
mForceDecorInstall = false;
mDecor = generateDecor(-1);
if (mContentParent == null) {
//R.id.content
mContentParent = generateLayout(mDecor);
final decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
//...省略一些decorContentParent的處理
} else {
mTitleView = findViewById(R.id.title);
final View titleContainer = findViewById(R.id.title_container);
///省略設(shè)置mTitle 設(shè)置標(biāo)題容器顯示隱藏
}
//設(shè)置decor背景
//省略activity各種動畫的實例化
}
}
這一塊除了一些標(biāo)題。動畫的初始化之外佳谦,最為關(guān)鍵的就是
- 通過
generateDecor()
生成了DecorView
- 以及通過
generateLayout()
獲取了ContentLayout
- 獲取windowStyle的各種屬性戴涝,并設(shè)置Features和WindowManager.LayoutParams.flags等
- 如果window是頂層容器,獲取背景資源等信息
- 獲取各種默認(rèn)布局實例化( R.layout.screen_simple等),加到DecorView中啥刻。和
AppComptDelegateImpl#createSubDecor
創(chuàng)建的subDecor
類似奸鸯。 - 獲取
com.android.internal.R.id.content
布局,并返回為ContentLayout
接下來再看Window#setContentView
了:
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
關(guān)鍵代碼很簡單可帽,就是往mContentParent
中添加view娄涩。而從上文可知,mContentParent就是andorid.R.id.content
的布局映跟。
3.5 小結(jié):
分析得知蓄拣,xml 編寫layout布局到展示布局在界面上,經(jīng)歷了這么個流程:
-
啟動activity
-
創(chuàng)建PhoneWindow
-
設(shè)置布局setContentView
- 確認(rèn)subDecorView的初始化
- 初始化生成DecorView
- Window中 創(chuàng)建DecorView
- Window中 創(chuàng)建樣例到代碼布局作為DecorView的子布局(比如R.layout.smple)
- 返回
com.android.internal.R.id.content
作為ContentPrent - Window中 處理
DecorContentParent
布局努隙,或者處理標(biāo)題等內(nèi)容
- 實例化subDecorView球恤,如R.layout.abc_screen_simple
- 設(shè)置
subDecorView
到Window的ContentPrent
- 初始化生成DecorView
- 添加實例化的Layout 到android.R.id.content
- 確認(rèn)subDecorView的初始化
-
addView的時候調(diào)用
requestLayout(); invalidate(true);
-
requestLayout
遍歷View樹到DecorView,調(diào)用ViewRootImpl#requestLayoutDuringLayout
-
invalidate
判斷區(qū)域內(nèi)的view荸镊,將需要刷新的view設(shè)置為dirty咽斧。
-
-
等待繪制時機(handleResumeActivity之后才會觸發(fā)繪制),通過
Choreographer
遍歷view樹的布局和繪制操作躬存。
據(jù)此算是完全搞清楚了setContentView
的時候經(jīng)歷了什么张惹。也明白了activity如何根據(jù)float, title等屬性生成不同的布局了岭洲。
最后
這一篇詳細(xì)介紹了view
的繪制系統(tǒng)宛逗,同時也是window窗口機制以及 android顯示機制的前置知識。view系統(tǒng)是我們ui開發(fā)過程中接觸最深的android知識盾剩。了解繪制原理不止對面試有幫助雷激。對于自己的開發(fā)工作也有不小的助力。