【Android進(jìn)階】這一次把View繪制流程刻在腦子里!扁位!

天空看不見云准潭,大火球在上面肆意發(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窗體機制以及繪制流程有所了解兜叨。

  1. 流程圖分析
  2. 了解view繪制流程
  3. 了解setContentView如何附加到內(nèi)容到頁面

關(guān)鍵類解釋

  • Choreographer:協(xié)調(diào)動畫穿扳、輸入和繪圖的時間。Choreographer從顯示子系統(tǒng)接收定時脈沖(例如垂直同步)国旷,然后安排工作發(fā)生矛物,作為渲染下一個顯示幀的一部分。

一. 流程圖分析

1.1 創(chuàng)建Activity到setContentView的窗口附加流程圖

下圖展示了window的創(chuàng)建到setContentView之后的窗體view樹變化情況

activity 設(shè)置布局流程

1.2 view繪制流程圖

繪制流程圖

二. view繪制流程

2.1 繪制流程分析

在我們調(diào)用requestLayoutinvalidate的時候跪但,我們會讓view刷新布局和繪制履羞。所以從這兩個方法入手,可以完整地走一遍繪制流程屡久。
繪制動畫等行為主要通過Choreographer 類協(xié)調(diào)忆首。

  1. 調(diào)用requestLayoutinvalidate標(biāo)記繪制和充布局信息
  2. Choreographer接受系統(tǒng)垂直同步等脈沖消息,在scheduleTraversals方法中回調(diào)執(zhí)行doTraversal 開始遍歷view樹涂身。
  3. 觸發(fā)ViewRootImpl#performTraversals完成view樹遍歷
    1. 如果layoutRequested 為true,measureHierarchy 中測量 mView 及其子view
    2. 需要的話,觸發(fā)ViewRootImpl#performLayout 完成布局
    3. 如果view沒有隱藏且TreeObserver中沒有攔截繪制搓蚪,就調(diào)用performDraw蛤售,完成繪制
      1. 計算dirty臟區(qū)域
      2. 從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)鍵流程如下:

  1. Activity#handleResume 的時候漠酿,調(diào)用WIndowManager#addView添加decorView
  2. 調(diào)用到WindowManagerGlobal#addView 的時候創(chuàng)建ViewRootImpl實例。
  3. 調(diào)用ViewRootImpl#setView完成一系列初始化方法
    1. 注冊mDisplayListenerDisplayManager谎亩,接收顯示更新回調(diào)
    2. 調(diào)用 requestLayout更新一次布局大小和位置信炒嘲,以確保從系統(tǒng)接收任何其他事件之前進(jìn)行過一次布局
    3. 通過WindowSession調(diào)用addToDisplayAsUser,添加window
  4. 在接收系統(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)容桶现,有如下幾個步驟:

  1. 啟動activity躲雅,在performLaunchActivity的時候創(chuàng)建Activity并且attach和調(diào)用onCreate方法
  2. 在attach的時候,創(chuàng)建PhoneWindow實例并持有mWindow引用
  3. 調(diào)用setContentView 以附加內(nèi)容到windows中
  4. 通過確認(rèn)decorView以及 subDecorView存在巩那,創(chuàng)建DecorViewsubDecorView
  5. 添加ContentViewdecorView樹中的 R.id.content節(jié)點
  6. 當(dāng)handleResumeActivity的時候吏夯,調(diào)用WindowManager.addView。關(guān)聯(lián)ViewViewRootImpl即横,后續(xù)便可以繪制噪生。

3.1 創(chuàng)建PhoneWindow

我們先看啟動activity的方法,ActivityThread#performLaunchAcivity东囚。 從該方法源碼中可知跺嗽,啟動activity的方法流程如下:

  1. 創(chuàng)建Activity實例 ,在Instrumentation#newActivity完成
  2. 創(chuàng)建PhoneWindows附加到Activity页藻。在Activity#attachAcitivity完成
  3. 調(diào)用Activity的onCreate生命周期,代碼是Instrumentation#callActivityOnCreate
  4. 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#installDecorWindow#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)歷了這么個流程:

  1. 啟動activity

  2. 創(chuàng)建PhoneWindow

  3. 設(shè)置布局setContentView

    1. 確認(rèn)subDecorView的初始化
      1. 初始化生成DecorView
        1. Window中 創(chuàng)建DecorView
        2. Window中 創(chuàng)建樣例到代碼布局作為DecorView的子布局(比如R.layout.smple)
        3. 返回 com.android.internal.R.id.content 作為ContentPrent
        4. Window中 處理DecorContentParent布局努隙,或者處理標(biāo)題等內(nèi)容
      2. 實例化subDecorView球恤,如R.layout.abc_screen_simple
      3. 設(shè)置 subDecorView到Window的ContentPrent
    2. 添加實例化的Layout 到android.R.id.content
  4. addView的時候調(diào)用 requestLayout(); invalidate(true);

    1. requestLayout遍歷View樹到DecorView,調(diào)用ViewRootImpl#requestLayoutDuringLayout
    2. invalidate 判斷區(qū)域內(nèi)的view荸镊,將需要刷新的view設(shè)置為dirty咽斧。
  5. 等待繪制時機(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ā)工作也有不小的助力。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彪腔,一起剝皮案震驚了整個濱河市侥锦,隨后出現(xiàn)的幾起案子进栽,更是在濱河造成了極大的恐慌德挣,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件快毛,死亡現(xiàn)場離奇詭異格嗅,居然都是意外死亡,警方通過查閱死者的電腦和手機唠帝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門屯掖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人襟衰,你說我怎么就攤上這事贴铜。” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵绍坝,是天一觀的道長徘意。 經(jīng)常有香客問我,道長轩褐,這世上最難降的妖魔是什么椎咧? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮把介,結(jié)果婚禮上勤讽,老公的妹妹穿的比我還像新娘。我一直安慰自己拗踢,他們只是感情好脚牍,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秒拔,像睡著了一般莫矗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砂缩,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天作谚,我揣著相機與錄音,去河邊找鬼庵芭。 笑死妹懒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的双吆。 我是一名探鬼主播眨唬,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼好乐!你這毒婦竟也來了匾竿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蔚万,失蹤者是張志新(化名)和其女友劉穎岭妖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體反璃,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡昵慌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了淮蜈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斋攀。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖梧田,靈堂內(nèi)的尸體忽然破棺而出淳蔼,到底是詐尸還是另有隱情侧蘸,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布鹉梨,位于F島的核電站闺魏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏俯画。R本人自食惡果不足惜析桥,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望艰垂。 院中可真熱鬧泡仗,春花似錦、人聲如沸猜憎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胰柑。三九已至截亦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間柬讨,已是汗流浹背崩瓤。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留踩官,地道東北人却桶。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像蔗牡,于是被迫代替她去往敵國和親颖系。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容