
天空看不見云,大火球在上面肆意發(fā)光,逼著毛孔慢慢滲出汗水。
我離開舒適區(qū),跑出去面試了幾次。
得到的最多的反饋是不夠深入。
作為一個(gè)五年經(jīng)驗(yàn)的安卓開發(fā)者,欠缺的還有很多。
前言
從一個(gè)view實(shí)例被創(chuàng)建,到展示到屏幕上,都經(jīng)歷了怎么樣的一個(gè)流程?在安卓開發(fā)中,這似乎是一個(gè)基本的知識(shí),應(yīng)該被開發(fā)者清楚地認(rèn)識(shí)明白,面試中也作為問題頻頻出現(xiàn),然而我還是認(rèn)識(shí)得不深刻。
Android View的繪制流程 是View相關(guān)的核心知識(shí)點(diǎn)。我希望通過這篇文章學(xué)習(xí)并分享Android View繪制流程的始末。
并將其刻在腦子里。
目錄
本文分為以下流程學(xué)習(xí),閱讀完本文將會(huì)學(xué)習(xí)到PhoneWindow,WindowManger,ViewRootImpl,View 等關(guān)鍵類的聯(lián)系和作用。對(duì)window窗體機(jī)制以及繪制流程有所了解。
- 流程圖分析
- 了解view繪制流程
- 了解
setContentView如何附加到內(nèi)容到頁面
關(guān)鍵類解釋
-
Choreographer:協(xié)調(diào)動(dòng)畫、輸入和繪圖的時(shí)間。Choreographer從顯示子系統(tǒng)接收定時(shí)脈沖(例如垂直同步),然后安排工作發(fā)生,作為渲染下一個(gè)顯示幀的一部分。
一. 流程圖分析
1.1 創(chuàng)建Activity到setContentView的窗口附加流程圖
下圖展示了window的創(chuàng)建到setContentView之后的窗體view樹變化情況
1.2 view繪制流程圖
二. view繪制流程
2.1 繪制流程分析
在我們調(diào)用requestLayout 和 invalidate的時(shí)候,我們會(huì)讓view刷新布局和繪制。所以從這兩個(gè)方法入手,可以完整地走一遍繪制流程。
繪制動(dòng)畫等行為主要通過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中測(cè)量mView及其子view - 需要的話,觸發(fā)
ViewRootImpl#performLayout完成布局 - 如果
view沒有隱藏且TreeObserver中沒有攔截繪制,就調(diào)用performDraw,完成繪制- 計(jì)算dirty臟區(qū)域
- 從mSurface中 獲取臟區(qū)域的canvas,交給view繪制
- 如果
2.2 ViewRootImpl 創(chuàng)建時(shí)機(jī)
從上面可以看到,所有的繪制和布局都是由ViewRootImpl#doTraversal觸發(fā),然后對(duì)其持有的view樹進(jìn)行遍歷繪制。所以一定要了解ViewRootImpl和其持有的DecorView的創(chuàng)建和關(guān)聯(lián)時(shí)機(jī)。關(guān)鍵流程如下:
-
Activity#handleResume的時(shí)候,調(diào)用WIndowManager#addView添加decorView - 調(diào)用到
WindowManagerGlobal#addView的時(shí)候創(chuàng)建ViewRootImpl實(shí)例。 - 調(diào)用
ViewRootImpl#setView完成一系列初始化方法- 注冊(cè)
mDisplayListener到DisplayManager,接收顯示更新回調(diào) - 調(diào)用
requestLayout更新一次布局大小和位置信,以確保從系統(tǒng)接收任何其他事件之前進(jìn)行過一次布局 - 通過
WindowSession調(diào)用addToDisplayAsUser,添加window
- 注冊(cè)
- 在接收系統(tǒng)事件的時(shí)候,調(diào)用scheduleTraversals 繪制view樹
WindowMangerGlobal 最終調(diào)用的其實(shí)都是ViewRootImpl方法。ViewRootImpl在addView關(guān)聯(lián)號(hào)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)我們啟動(dòng)activity,將我們寫的xml布局文件顯示在屏幕上,其中經(jīng)歷了那些過程呢?我們要在界面上展示內(nèi)容,有如下幾個(gè)步驟:
- 啟動(dòng)activity,在
performLaunchActivity的時(shí)候創(chuàng)建Activity并且attach和調(diào)用onCreate方法 - 在attach的時(shí)候,創(chuàng)建PhoneWindow實(shí)例并持有mWindow引用
- 調(diào)用
setContentView以附加內(nèi)容到windows中 - 通過確認(rèn)
decorView以及subDecorView存在,創(chuàng)建DecorView和subDecorView - 添加
ContentView到decorView樹中的R.id.content節(jié)點(diǎn) - 當(dāng)
handleResumeActivity的時(shí)候,調(diào)用WindowManager.addView。關(guān)聯(lián)View和ViewRootImpl,后續(xù)便可以繪制。
3.1 創(chuàng)建PhoneWindow
我們先看啟動(dòng)activity的方法,ActivityThread#performLaunchAcivity。 從該方法源碼中可知,啟動(dòng)activity的方法流程如下:
- 創(chuàng)建Activity實(shí)例 ,在
Instrumentation#newActivity完成 - 創(chuàng)建PhoneWindows附加到Activity。在
Activity#attachAcitivity完成 - 調(diào)用Activity的onCreate生命周期,代碼是
Instrumentation#callActivityOnCreate - 在
onCreate中執(zhí)行用戶自定義的代碼,比如setContentView。
所以可知,在activity準(zhǔn)備啟動(dòng)的時(shí)候,就已經(jīng)完成了PhoneWindows實(shí)例的創(chuàng)建。而接下來就執(zhí)行到了我們?cè)?code>Activity#onCreate中調(diào)用setContentView方法設(shè)置的自定義布局。
3.2 setContentView的本質(zhì)
activity在啟動(dòng)之后,我們通常在onCreate調(diào)用setContentView中設(shè)置自己的布局文件。我們來具體看看setContentView做了什么。
setContentView方法本質(zhì)其實(shí)是向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的過程自然會(huì)觸發(fā)布局的重新渲染。
關(guān)鍵之處還是在于ensureSubDecor()方法中對(duì)于decoView以及subDecorView的實(shí)例化創(chuàng)建工作。
3.3 確認(rèn)window ,decorView 以及 subDecorView
先看看AppCompatDelegateImpl#ensureSubDecor()的主要實(shí)現(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布局的實(shí)例化
//包含 actionBar floatTitle ActionMode等樣式
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
//省略狀態(tài)欄適配代碼
//省略actionBar布局替換代碼
mWindow.setContentView(subDecor);
return subDecor;
}
代碼很長(zhǎng),上面是經(jīng)過省略之后的主要代碼。可以看到代碼邏輯很清晰:
- 步驟一:確認(rèn)window并attach(設(shè)置背景等操作)
- 步驟二:獲取DecorView,因?yàn)槭堑谝淮握{(diào)用所以會(huì)installDecor(創(chuàng)建DecorView和Window#ContentLyout)
- 步驟三:從xml中實(shí)例化出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各種動(dòng)畫的實(shí)例化
}
}
這一塊除了一些標(biāo)題。動(dòng)畫的初始化之外,最為關(guān)鍵的就是
- 通過
generateDecor()生成了DecorView - 以及通過
generateLayout()獲取了ContentLayout- 獲取windowStyle的各種屬性,并設(shè)置Features和WindowManager.LayoutParams.flags等
- 如果window是頂層容器,獲取背景資源等信息
- 獲取各種默認(rèn)布局實(shí)例化( 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)鍵代碼很簡(jiǎn)單,就是往mContentParent中添加view。而從上文可知,mContentParent就是andorid.R.id.content的布局。
3.5 小結(jié):
分析得知,xml 編寫layout布局到展示布局在界面上,經(jīng)歷了這么個(gè)流程:
-
啟動(dò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)容
- 實(shí)例化subDecorView,如R.layout.abc_screen_simple
- 設(shè)置
subDecorView到Window的ContentPrent
- 初始化生成DecorView
- 添加實(shí)例化的Layout 到android.R.id.content
- 確認(rèn)subDecorView的初始化
-
addView的時(shí)候調(diào)用
requestLayout(); invalidate(true);-
requestLayout遍歷View樹到DecorView,調(diào)用ViewRootImpl#requestLayoutDuringLayout -
invalidate判斷區(qū)域內(nèi)的view,將需要刷新的view設(shè)置為dirty。
-
-
等待繪制時(shí)機(jī)(handleResumeActivity之后才會(huì)觸發(fā)繪制),通過
Choreographer遍歷view樹的布局和繪制操作。
據(jù)此算是完全搞清楚了setContentView的時(shí)候經(jīng)歷了什么。也明白了activity如何根據(jù)float, title等屬性生成不同的布局了。
最后
這一篇詳細(xì)介紹了view的繪制系統(tǒng),同時(shí)也是window窗口機(jī)制以及 android顯示機(jī)制的前置知識(shí)。view系統(tǒng)是我們ui開發(fā)過程中接觸最深的android知識(shí)。了解繪制原理不止對(duì)面試有幫助。對(duì)于自己的開發(fā)工作也有不小的助力。