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

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

  1. 流程圖分析
  2. 了解view繪制流程
  3. 了解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樹變化情況

activity 設(shè)置布局流程

1.2 view繪制流程圖

繪制流程圖

二. view繪制流程

2.1 繪制流程分析

在我們調(diào)用requestLayoutinvalidate的時(shí)候,我們會(huì)讓view刷新布局和繪制。所以從這兩個(gè)方法入手,可以完整地走一遍繪制流程。
繪制動(dòng)畫等行為主要通過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 中測(cè)量 mView 及其子view
    2. 需要的話,觸發(fā)ViewRootImpl#performLayout 完成布局
    3. 如果view沒有隱藏且TreeObserver中沒有攔截繪制,就調(diào)用performDraw,完成繪制
      1. 計(jì)算dirty臟區(qū)域
      2. 從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)鍵流程如下:

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

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

3.1 創(chuàng)建PhoneWindow

我們先看啟動(dòng)activity的方法,ActivityThread#performLaunchAcivity。 從該方法源碼中可知,啟動(dòng)activity的方法流程如下:

  1. 創(chuàng)建Activity實(shí)例 ,在Instrumentation#newActivity完成
  2. 創(chuàng)建PhoneWindows附加到Activity。在Activity#attachAcitivity完成
  3. 調(diào)用Activity的onCreate生命周期,代碼是Instrumentation#callActivityOnCreate
  4. 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#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各種動(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è)流程:

  1. 啟動(dòng)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. 實(shí)例化subDecorView,如R.layout.abc_screen_simple
      3. 設(shè)置 subDecorView到Window的ContentPrent
    2. 添加實(shí)例化的Layout 到android.R.id.content
  4. addView的時(shí)候調(diào)用 requestLayout(); invalidate(true);

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

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

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

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