Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android中不得不談的setContentView

Android中不得不談的setContentView

編輯:關於Android編程

寫在前面:

幾個月之前在做項目的布局優化時,使用 Hierarchy Viewer 查看項目的層級結構,然後發現頂層的布局並不是在XML中我寫的根布局,而是嵌套了多層 Layout ,簡單查閱了一些資料之後明白這是系統為我們加上的。把這個知識點寫在了印象筆記中的 TODO list(裡面還有好多知識想研究,一直在拖延T.T),擱置了好久最近重新拿出來好好研究了一下,爭取做到溫故知新,融會貫通嘛。

也許有的同學沒看過 Hierarchy Viewer 下項目的界面布局,沒關系,我現在帶大家了解下。
新建一個 module ,打開 sdk tool 文件夾下的 Hierarchy Viewer ,布局結構展示如下:

先別著急找放大鏡,想想我們新建項目的默認布局,按理說根布局應該是 RelativeLayout ,並且子 View 是一個 TextView 寫著 “Hello World”才對啊~ 多出來的這些布局層級是什麼

既然陌生又看不懂,那就先從我們熟悉的入手,找一下我們自己寫的布局:

原來 RelativeLayout 和它的子 View TextView 在這裡,看一下左下角的位置標識,紅框部分指明 RelativeLayout 是 Toolbar 以下的部分。

再想想,我們是通過什麼方法將這個布局填充到 Activity 上的呢?

沒錯是 setContentView

那就在 setContentView 中尋找蛛絲馬跡吧

因為在 Android Studio 中 MainActivity 默認繼承於v7包下的 AppCompatActivity ,目的是為了提供控件的向下兼容或者新控件,AppCompatActivity 也是層層繼承於 Activity ,所以我們直接去看 ActivitysetContentView

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

getWindow 拿到了 Activity 的成員變量 mWindow ,進而調用了 setContentView 方法,mWindow 是 Window 類,繼續跟進,看看 Window類 是什麼

注釋中的描述翻譯過來就是,Window 是 視覺和行為表現的頂層抽象基類,它的實例會當作頂層視圖添加進 WindowManager , 它有一個唯一的實現類是 PhoneWindow
本文我們不會去剖析 WindowManager 有哪些作用和行為,我默默地把它加入了我的 TODO list 中,拖延到什麼時候就不一定了哈T.T。

為了防止你忘了我們在做什麼和我們即將做什麼,先來一個中場回顧
首先我們查看布局時發現有很多“超出我們預料和理解范疇”的布局出現,跟進 setContentView 方法,發現 Acitvity 中是 Window 調用了 setContentView ,而抽象基類 Window 有一個唯一的實現類 PhoneWindow。不多說,來看看實現類 PhoneWindow 中的 setContentView 方法。

    @Override
    public void setContentView(int layoutResID) {
        // 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) {
            //初始化 DectorView 和 mContentParent
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //首次 setContentView 走到這裡
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

當我們沒有調用 setContentView 時,mContentParent (是ViewGroup) 是 null ,所以有兩行代碼值得我們關注 installDecor() 和 mLayoutInflater.inflate(layoutResID, mContentParent)
首先 mContentParent 作為第二個參數傳入了 inflate 方法中, 也就是說 我的布局中的 RelativeLayout 被層層解析之後的 View 視圖樹 作為了 mContentParent 的子 View 插入。

現在不知道 mContentParent 是什麼沒關系,繼續跟進 installDecor() 方法。

(隨著API level的升高,源碼發生了很多有關 Feature 、 style 和 Wiget 的細微變化,還是蠻有意思的)
(這裡我還想說一句,相信在 Android 設計之初 PhoneWindow 這個類就存在了,顯然現在的這個命名有些問題,畢竟目前的設備不僅僅是 phone 了,也許改成 DeviceWindow 會比較合適)

    private void installDecor() {
        if (mDecor == null) {
            // new 一個 DecorView
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        }
        if (mContentParent == null) {
            //初始化 mContentParent 
            mContentParent = generateLayout(mDecor);
            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();
            // 找到一個帶ActionBar屬性的布局容器 decorContentParent 
            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());             
                //配置UI設置
                mDecorContentParent.setUiOptions(mUiOptions);
            }
         } else {
             if (mContentParent instanceof FrameLayout) {
                  ((FrameLayout)mContentParent).setForeground(null);
                }
         }                   
    }

省略了與分析無關的代碼,其中很多是對 Feature 和 style 屬性的一些判斷和設置,首先 installDecor() 方法從字面意思看,很有可能是初始化加載 DecorView 的,首先看看 PhoneWindow 中兩個成員變量 mDecormContentParent 分別是什麼:

描述的信息可以概括為 mDector 是 窗體的頂級視圖,mContentParent 是放置窗體內容的容器,也就是我們 setContentView 時,所加入的 View 視圖樹。

當二者為 null 時,有兩行代碼值得關注,分別為 mDecor = generateDecor() 和 mContentParent = generateLayout(mDecor)

不過在此之前,先來看看這行尋找decorContentParent布局的代碼

final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

decor_content_parent 看起來很眼熟的樣子,點擊它進入布局來看看:

screen_toolbar.xml

為什麼說 decor_content_parent 眼熟呢?打開布局查看器來看看

這裡寫圖片描述

Hierarchy Viewer 中可以看到 ActionBarOverlayLayout 的布局文件的 id 正是 decor_content_parent 不光如此 布局文件中的每個 View 節點的名稱和 id 都與 Hierarchy Viewer<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPiDK08281tC1xNK70ru21NOmoaPU2b+0xuTW0LXEIEZyYW1lTGF5b3V0ILXEIGlkIM6qIGNvbnRlbnQgo6wgztLDx9fUyLu2+Mi7tcSywrLiy/y+zcrHztLDx7j5sry+1iBSZWxhdGl2ZUxheW91dCC1xDxzdHJvbmc+uLiyvL7WPC9zdHJvbmc+o6zQxMDv0rvPwtPQwcu116OsvMzQ+NHQvr9+PC9wPg0KPHA+uPq9+CBnZW5lcmF0ZURlY29yKCkgt723qKO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;"> protected DecorView generateDecor() { return new DecorView(getContext(), -1); }

這個沒什麼可多說的,就是為我們的窗體 new 了 一個 DecorView 。

再來看 generateLayout(mDecor)

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        // 獲得窗體的 style 樣式
        TypedArray a = getWindowStyle();

        // 省略大量無關代碼

        // Inflate the window decor.
        int layoutResource;
        int features = getLocalFeatures();

        //填充帶有 style 和 feature 屬性的 layoutResource (是一個layout id)

        View in = mLayoutInflater.inflate(layoutResource, null);

        // 插入的頂層布局 DecorView 中

        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        // 找到我們XML文件的父布局 contentParent 

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        // 省略無關代碼
        mDecor.finishChanging();
        // 返回 contentParent 並賦值給成員變量 mContentParent
        return contentParent;
    }

這個方法的代碼有300多行,剔除了很多無關代碼,我們分模塊來看:

    View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

首先 layoutResource 是系統的xml布局文件的id,裡面有我們設置窗體的 features 和 style 屬性,然後通過decor.addView 添加進 mDector 視圖。這裡也是我們要在 setContentView() 之前執行requestWindowFeature()才可以的原因

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.

        mDecor.finishChanging();

        return contentParent;

關鍵點來了, ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
通過 findViewById 找到系統修飾布局文件中 id 為:

這個 id 是不是非常眼熟,與我們上文的猜測不謀而合,這就是我們一直在尋找的作為 activity_main 的父布局的 FrameLayout

我們在布局文件查看器中再找一下:

容器 FrameLayout id = content

return contentParent 這一步就返回了我們的成員變量 mContentParent

到現在為止其實整個知識點主干的邏輯已經走完了,為大家花了一張簡單的思維導圖

並不復雜,線性邏輯調用還是蠻清晰的。

不過相信你也許會問,上文你僅僅提到了兩個布局呀,一個頂層的 DecorView 和 我們布局文件的父布局 FrameLayout ,而查看布局層級時,為什麼有這麼多其他這麼多額外的布局呢?

因為隨著 Android API level 的不斷變化,組件也在隨之增多,比如ActionBar Toolbar等等,這些組件相關的布局是否加載與你的 feature 設置設備的特性相關聯,而且版本不同,布局文件的層級結構也在不斷變化著豐富著,我這個是 API22 的源碼,我做了一些對比,有許多代碼細節是不一樣的,比如在這裡的 Feature 就新增了 Toolbar ,但是大體上的邏輯框架肯定不會變
比如我們目前的 MainActivity 的視圖主要有兩大分支,一條設置 Toolbar 的相關配置,一條就是我們的 RelativeLayout 了。

寫在後面:
寫這篇博客的原因一是我自己要研究梳理總結這個知識點,二是想讓大家明白,Android版本之間的迭代很快,一年前的博客闡述的觀點到今天可能就再不適用了,但是 PhoneWindow 管理布局視圖的這套邏輯框架,卻一直沒怎麼改變。通過閱讀源碼,可以學習 Google 工程師們良好的代碼風格,汲取他們搭建框架的思想,讓我們自己寫的代碼也能如此健壯。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved