寫在前面:
幾個月之前在做項目的佈局優化時,使用 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 ,所以我們直接去看 Activity 的 setContentView
/** * 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(); }1234567891011121312345678910111213
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(); } }1234567891011121314151617181920212223242512345678910111213141516171819202122232425
當我們沒有調用 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); } } }123456789101112131415161718192021222324252627123456789101112131415161718192021222324252627
省略了與分析無關的代碼,其中很多是對 feature 和 style 屬性的一些判斷和設置,首先 installDecor()
方法從字面意思看,很有可能是初始化加載 DecorView 的,首先看看 PhoneWindow 中兩個成員變量 mDecor 和 mContentParent 分別是什麼:
描述的信息可以概括爲 mDector 是 窗體的頂級視圖,mContentParent 是放置窗體內容的容器,也就是我們 setContentView()
時,所加入的 View 視圖樹。
當二者爲 null 時,有兩行代碼值得關注,分別爲 mDecor = generateDecor()
和 mContentParent = generateLayout(mDecor)
不過在此之前,先來看看這行尋找 decorContentParent 佈局的代碼
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent);1212
decor_content_parent 看起來很眼熟的樣子,點擊它進入佈局來看看:
爲什麼說 decor_content_parent 眼熟呢?打開佈局查看器來看看
在 Hierarchy Viewer 中可以看到 ActionBarOverlayLayout 的佈局文件的 id 正是 decor_content_parent 不光如此 佈局文件中的每個 View 節點的名稱和 id 都與 Hierarchy Viewer 視圖中的一一對應。再看其中的 FrameLayout 的 id 爲 content , 我們自然而然的猜測它就是我們根佈局 RelativeLayout 的父佈局,心裏一下有了底,繼續研究~
跟進 generateDecor() 方法:
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }123123
這個沒什麼可多說的,就是爲我們的窗體 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; }1234567891011121314151617181920212223242526272829303112345678910111213141516171819202122232425262728293031
這個方法的代碼有300多行,剔除了很多無關代碼,我們分模塊來看:
View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in;123123
首先 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;12345678910111234567891011
關鍵點來了, ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
通過 findViewById 找到系統修飾佈局文件中 id 爲:
這個 id 是不是非常眼熟,與我們上文的猜測不謀而合,這就是我們一直在尋找的作爲 activity_main 的父佈局的 FrameLayout
我們在佈局文件查看器中再找一下:
return contentParent 這一步就返回了我們的成員變量 mContentParent
到現在爲止其實整個知識點主幹的邏輯已經走完了,爲大家花了一張簡單的思維導圖
並不複雜,線性邏輯調用還是蠻清晰的。
不過相信你也許會問,上文你僅僅提到了兩個佈局呀,一個頂層的 DecorView 和 我們佈局文件的父佈局 FrameLayout ,而查看佈局層級時,爲什麼有這麼多其他這麼多額外的佈局呢?
因爲隨着 Android API level 的不斷變化,組件也在隨之增多,比如 ActionBar Toolbar 等等,這些組件相關的佈局是否加載與你的 feature 設置設備的特性相關聯,而且版本不同,佈局文件的層級結構也在不斷變化着豐富着,我這個是 API22 的源碼,我做了一些對比,有許多代碼細節是不一樣的,比如在這裏的 feature 就新增了 Toolbar ,但是大體上的邏輯框架肯定不會變
比如我們目前的 MainActivity 的視圖主要有兩大分支,一條設置 Toolbar 的相關配置,一條就是我們的 RelativeLayout 了。
寫在後面:
寫這篇博客的原因一是我自己要研究梳理總結這個知識點,二是想讓大家明白,Android 版本之間的迭代很快,一年前的博客闡述的觀點到今天可能就再不適用了,但是 PhoneWindow 管理佈局視圖的這套邏輯框架,卻一直沒怎麼改變。通過閱讀源碼,可以學習 Google 工程師們良好的代碼風格,汲取他們搭建框架的思想,讓我們自己寫的代碼也能如此健壯。
PS: PhoneWindow 什麼時候能改個名字啊!