Android系統源碼分析--View繪制流程之-setContentView

上一篇分析了四大組件之ContentProvider,這也是四大組件最后一個。因此,從這篇開始我們分析新的篇章--View繪制流程,View繪制流程在Android開發中占有非常重要的位置,只要有視圖的顯示,都離不開View的繪制,所以了解View繪制原理對于應用開發以及系統的學習至關重要。由于View繪制流程比較復雜,并且涉及的知識非常多,所以后面我會按照下面幾方面來介紹View的繪制流程。每篇不是很長,但是盡量的詳細,讓每個人都看懂。

  • Android系統源碼分析--View繪制流程之-setContentView
  • Android系統源碼分析--View繪制流程之-inflate
  • Android系統源碼分析--View繪制流程之-onMeasure
  • Android系統源碼分析--View繪制流程之-onLayout
  • Android系統源碼分析--View繪制流程之-onDraw
  • Android系統源碼分析--View繪制流程之-硬件加速
  • Android系統源碼分析--View繪制流程之-addView
  • Android系統源碼分析--View繪制流程之-彈性效果

所以這篇我們先分析View繪制流程的setContentView方法,按照慣例,先貼一下流程圖:

setContentView.jpg

1.PhoneWindow.setContentView

調用setContentView最開始的地方是在我們繼承Activity的子類中的onCreate方法中,這個方法其實是調用的Activity中的setContentView方法:

    public void setContentView(@LayoutRes int layoutResID) {
        // getWindow獲取的是PhoneWindow,所以這里是調用的PhoneWindow的setContentView方法
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

其實這個getWindow獲取的是繼承Window的PhoneWindow,所以這里getWindow.setContentView是調用的PhoneWindow.setContentView方法,具體的自己可以看看代碼哪里賦值的就知道了。另外這個方法還有兩個類似的方法:

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

這三個方法差不多,只不過下面的兩個直接傳遞了view對象,而第一個是傳遞了view的id。我們接著看PhoneWindow.setContentView方法。

    public void setContentView(int layoutResID) {
        // 根據layout的id加載一個布局,然后通過findViewById(R.id.content)加載出布局中id為content
        // 的FrameLayout賦值給mContentParent,并且將該view添加到mDecor(DecorView)中
        if (mContentParent == null) {// 第一次是空
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            // 沒有過度效果,并且不是第一次setContentView,那么要先移除盛放setContentView傳遞進來
            // 的View的父容器中的所有子view
            mContentParent.removeAllViews();
        }

        // 窗口是否需要過度顯示
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        } else {// 不需要過度,加載id為layoutResID的視圖并且添加到mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        // 繪制視圖
        mContentParent.requestApplyInsets();
        ...
        mContentParentExplicitlySet = true;
    }

上面注釋很詳細,但是還是需要解釋一下mContentParent,這個mContentParent是一個FrameLayout,這里的Content是指你setContentView傳遞進來的id指向的視圖,所以mContentParent也就是指放置傳遞進來的視圖的父視圖。看下面的圖:

ContentView.png

上面的ActionBarContextView是標題,不過有些設置是不會顯示整個標題的,所以這里只是一種情況,下面的id為content的FrameLayout就是這個mContentParent,你通過setContentView方法傳遞的視圖會放到這個id為content的FrameLayout上面,這樣你的Activity就顯示了你寫的布局視圖了,這里先解釋一下,我們下面看看是不是真的這樣。由于第一次創建Activity時mContentParent是空的,所以會走PhoneWindow.installDecor方法。

2.PhoneWindow.installDecor

    private void installDecor() {
        mForceDecorInstall = false;
        // 繼承FrameLayout,是窗口頂級視圖,也就是Activity顯示View的根View,包含一個TitleView和一個ContentView
        if (mDecor == null) {// 首次為空
            // 創建DecorView(FrameLayout)
            mDecor = generateDecor(-1);
            ...
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {// 第一次setContentView時為空
            // 這個mContentParent就是后面從系統的frameworks\base\core\res\res\layout\目錄下加載出來
            // 的layout布局(這個Layout布局加載完成后會添加到mDecor(DecorView)中)中的一個id為content的
            // FrameLayout控件,這個FrameLayout控件用來盛放setContentView傳遞進來的View
            mContentParent = generateLayout(mDecor);

            ...

            // 判斷是否存在id為decor_content_parent的view(我只看到screen_action_bar.xml這個里面有這個id)
            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                ...
                if (mDecorContentParent.getTitle() == null) {
                    // 設置標題
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                ...
            } else {
                // 標題視圖
                mTitleView = (TextView) findViewById(R.id.title);
                // 有的布局中是沒有id為title的控件的,也就是不顯示標題
                if (mTitleView != null) {
                    // 判斷是否有不顯示標題的特性
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                        final View titleContainer = findViewById(R.id.title_container);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(View.GONE);
                        } else {
                            mTitleView.setVisibility(View.GONE);
                        }
                        mContentParent.setForeground(null);
                    } else {// 顯示標題
                        mTitleView.setText(mTitle);
                    }
                }
            }

            // 背景
            if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
                mDecor.setBackgroundFallback(mBackgroundFallbackResource);
            }

            // 過度效果
            ...
        }
    }

這里出現了一個mDecor,這個mDecor是DecorView,繼承FrameLayout,是窗口頂級視圖,也就是Activity顯示View的根View,包含一個TitleView和一個ContentView,也就是上面圖形中的最外層藍色的邊框所指代的視圖,當然,這里第一加載時也是空的,那么會調用generateDecor函數來創建mDecor,然后通過generateLayout方法創建mContentParent視圖,創建完成后會設置標題,設置標題的就不分析了,比較簡單,下面先看創建mDecor的方法。

3.PhoneWindow.generateDecor

    protected DecorView generateDecor(int featureId) {
        ...
        // activity.
        Context context;
        if (mUseDecorContext) {// 從Activity的setContentView方法調用則為true
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {// 系統進程時沒有Application的context,所以就用現有的context
                context = getContext();
            } else {// 應用會有application的Context
                ...
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

這里判斷了一個applicationContext是否存在,主要是區分這個是系統調用還是應用,系統是沒有applicationContext的,最后通過new關鍵字創建對象DecorView,這里就獲取到了DecorView。

5.PhoneWindow.generateLayout

    protected ViewGroup generateLayout(DecorView decor) {
        ...
        // 根據Window的屬性調用相應的requestFeature
        ...
        // 獲取Window的各種屬性來設置flag和參數
        ...
        // 根據之前的flag和feature來加載一個layout資源到DecorView中,并把可以作為容器的View返回
        // 這個layout布局文件在frameworks\base\core\res\res\layout\目錄下
        int layoutResource;
        int features = getLocalFeatures();
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                ...
            } else {
                layoutResource = R.layout.screen_title_icons;
            }
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            layoutResource = R.layout.screen_progress;
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            if (mIsFloating) {
                ...
            } else {
                layoutResource = R.layout.screen_custom_title;
            }
            ...
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            if (mIsFloating) {
                ...
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();
        // 根據layoutResource(布局id)加載系統中布局文件(Layout)并添加到DecorView中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        // contentParent是用來添加Activity中布局的父布局(FrameLayout),并帶有相關主題樣式,就是上面
        // 提到的id為content的FrameLayout,返回后會賦值給PhoneWindow中的mContentParent
        ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        ...
        // 設置mDecor背景之類
        ...
        mDecor.finishChanging();
        return contentParent;
    }

前面一大段if-else語句是根據屬性值獲取系統中的layout的id,主要有下面幾種:
* R.layout.screen_swipe_dismiss
* R.layout.screen_title_icons
* R.layout.screen_progress
* R.layout.screen_custom_title
* R.layout.screen_title
* R.layout.screen_simple_overlay_action_mode
* R.layout.screen_simple
這些布局文件都在系統frameworks\base\core\res\res\layout\目錄下,我們看其中一個screen_simple.xml布局代碼:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

其中我們需要獲取的contentParent就是xml布局中id為content的FrameLayout,為什么是這個,我們通過上面代碼分析,上面我們看到了mDecor.onResourcesLoaded方法,這里的第二個參數layoutResource就是上面的xml布局,所以這里就是加載這個布局的我們看看是不是

6.DecorView.onResourcesLoaded

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        ...
        // DecorView中的標題視圖,可能是空,也就是沒有標題
        mDecorCaptionView = createDecorCaptionView(inflater);
        // 加載Layout作為根布局(frameworks\base\core\res\res\layout\目錄下layout布局文件)
        // 這里獲取到的root是沒有寬高的
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {// 有標題
            // 這里可以看到mDecorCaptionView不為空時,將mDecorCaptionView添加到DecorView,然后再將
            // Layout添加到mDecorCaptionView
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {// 沒有標題
            // 如果mDecorCaptionView為空,則直接將跟布局Layout添加到DecorView
            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

這里首先創建了標題視圖,然后通過LayoutInflater.inflate加載了id為layoutResource的布局文件并賦值給root引用,最終返回的也是這個root,所以上面方法5中加載了這個布局文件,加載完成后,如果標題視圖文件存在,則將root添加到標題視圖中,再將標題視圖添加到DecorView上,如果沒有標題視圖,則直接將root布局添加到DecorView上面,寬高是MATCH_PARENT。再回到5中,在調用完mDecor.onResourcesLoaded方法后通過id為ID_ANDROID_CONTENT獲取了一個ViewGroup,那么這個ID_ANDROID_CONTENT是什么,通過查找我們發現是:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

這里就可以知道獲取的contentParent就是上面xml布局中的id為content的FrameLayout布局,所以到這里整體結構基本明白了。

另外上面調用了一個createDecorCaptionView方法并且傳入了LayoutInflater,那么看看這個方法做了哪些操作。

7.DecorView.createDecorCaptionView

    private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {
        ...
        if (!mWindow.isFloating() && isApplication && StackId.hasWindowDecor(mStackId)) {
            if (decorCaptionView == null) {
                decorCaptionView = inflateDecorCaptionView(inflater);
            }
            decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
        } else {
            decorCaptionView = null;
        }

        ...
        return decorCaptionView;
    }

這里其實就一個方法需要再看看那就是inflateDecorCaptionView方法。

8.DecorView.inflateDecorCaptionView

    private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) {
        final Context context = getContext();
        inflater = inflater.from(context);
        // 從frameworks\base\core\res\res\layout\中加載decor_caption.xml布局
        final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,
                null);
        ...
        return view;
    }

這里其實就是通過LayoutInflater.inflate方法加載frameworks\base\core\res\res\layout\下的decor_caption.xml布局,這個LayoutInflater.inflate由于比較重要,所以我們放到下一章單獨講解。

10.LayoutInflater.inflate

我們在第二步初始化完DecorView和mContentParent視圖后開始調用mLayoutInflater.inflate(layoutResID, mContentParent)方法,加載我們setContentView方法傳遞進來的視圖,也就是我們自己寫的Activity布局,之前的都是系統的布局。我們知道mContentParent是放置我們自己寫的Activity視圖的容器,所以后面就簡單了。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

上面我們說了這個方法具體分析我們下一章單獨分析。所以我們接著前面分析。

11.View.requestApplyInsets

    public void requestApplyInsets() {
        requestFitSystemWindows();
    }

12.View.requestFitSystemWindows

    public void requestFitSystemWindows() {
        if (mParent != null) {
            mParent.requestFitSystemWindows();
        }
    }

這里的mParent是ViewParent的具體實現ViewRootImpl,所以調用的是ViewRootImpl里的requestFitSystemWindows方法。

13.ViewRootImpl.requestFitSystemWindows

    public void requestFitSystemWindows() {
        checkThread();
        mApplyInsetsRequested = true;
        scheduleTraversals();// 繪制視圖
    }

checkThread這個是檢測線程的方法,也就是檢測當前線程是不是主線程,也就是setContentView方法要在UI線程調用。然后調用scheduleTraversals方法開始繪制視圖。

15.ViewRootImpl.scheduleTraversals

    void scheduleTraversals() {
        // 當mTraversalScheduled為false,也就是沒有重繪請求或者沒有未執行完的重繪時才開始重繪
        if (!mTraversalScheduled) {
            // 一旦開始重回此處設置為True,當執行完畢后調用unscheduleTraversals函數,
            // 重新設置為false,避免同時存在多次繪制
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 將消息放入消息處理器中,最終調用doTraversal方法
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }

mTraversalScheduled只有調用這個方法后才設置為true,所以在開始調用這個方法的時候是false,后面會將mTraversalRunnable放到消息處理器中,這個mTraversalRunnable是一個實現了Runnable接口的對象,所以從這里調用了TraversalRunnable中的run方法。

16.TraversalRunnable.run

        public void run() {
            doTraversal();
        }

這里很簡單就是調用了doTraversal方法。

17.ViewRootImpl.doTraversal

    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            ...

            // 執行View繪制流程
            performTraversals();

           ...
        }
    }

這里主要是調用performTraversals方法,開始View的真正繪制。

18.ViewRootImpl.performTraversals

    private void performTraversals() {
        
        ...

        // 這里是需要測量的條件:第一次加載View,需要調整窗口大小,需要適應系統窗口,視圖顯示狀態改變,
        // 視圖布局參數不為空,強制窗口重新布局。首先要滿足這個幾個條件才可能執行測量
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
           
            ...

            // 窗口沒有停止,或者通知需要繪制
            if (!mStopped || mReportNextDraw) {
                
                ...
                
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    ...

                    // 1.第一步:測量
                    // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    ...
                }
            }
        } else {
            ...
        }

        ...
        
        if (didLayout) {// 執行布局
            // 2.第二步:布局
            performLayout(lp, mWidth, mHeight);
            ...
        }
        
        ...
        
        // 如果沒有取消繪制,并且不是新的Surface,那么執行繪制
        if (!cancelDraw && !newSurface) {
            ...

            // 3.第三步:繪制
            performDraw();
        } else {// 如果取消了繪制或者是新的Surface,那么要重新測量、布局和繪制
            ...
        }

        mIsInTraversal = false;
    }

這里開始進入測量,布局,繪制的過程,里面通過各個條件來判斷需要執行哪一步或者哪幾部,因為這一段主要是設計測量、布局、繪制,所以這章就不分析了,這個方法放到《Android系統源碼分析--View繪制流程之-onMeasure》一章講解。

我們下一章開始分析《Android系統源碼分析--View繪制流程之-inflate》。

參考文章:

代碼地址:

直接拉取導入開發工具(Intellij idea或者Android studio)

由于coding與騰訊云合作,改變很多,所以后續代碼切換到Gitlab。

Android_Framework_Source

注:本文原創,轉載請注明出處,多謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容