本文原創(chuàng),轉(zhuǎn)載請注明出處。
歡迎關(guān)注我的 簡書 ,關(guān)注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質(zhì)量的Android相關(guān)博文。
寫在前面:
幾個月之前在做項目的布局優(yōu)化時,使用 Hierarchy Viewer 查看項目的層級結(jié)構(gòu),然后發(fā)現(xiàn)頂層的布局并不是在XML中我寫的根布局,而是嵌套了多層 Layout ,簡單查閱了一些資料之后明白這是系統(tǒng)為我們加上的。把這個知識點寫在了印象筆記中的 TODO list(里面還有好多知識想研究,一直在拖延T.T),擱置了好久最近重新拿出來好好研究了一下,爭取做到溫故知新,融會貫通嘛。
也許有的同學(xué)沒看過 Hierarchy Viewer 下項目的界面布局,沒關(guān)系,我現(xiàn)在帶大家了解下。
新建一個 module ,打開 sdk tool 文件夾下的 Hierarchy Viewer ,布局結(jié)構(gòu)展示如下:
先別著急找放大鏡,想想我們新建項目的默認布局,按理說根布局應(yīng)該是 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();
}
getWindow() 拿到了 Activity 的成員變量 mWindow ,進而調(diào)用了 setContentView() 方法,mWindow 是 Window 類,繼續(xù)跟進,看看 Window 類是什么
注釋中的描述翻譯過來就是,Window 是 視覺和行為表現(xiàn)的頂層抽象基類,它的實例會當作頂層視圖添加進 WindowManager , 它有一個唯一的實現(xiàn)類是 PhoneWindow。
本文我們不會去剖析 WindowManager 有哪些作用和行為,我默默地把它加入了我的 TODO list 中,拖延到什么時候就不一定了哈T.T。
為了防止你忘了我們在做什么和我們即將做什么,先來一個中場回顧:
首先我們查看布局時發(fā)現(xiàn)有很多“超出我們預(yù)料和理解范疇”的布局出現(xiàn),跟進 setContentView() 方法,發(fā)現(xiàn) Acitvity 中是 Window 調(diào)用了 setContentView() ,而抽象基類 Window 有一個唯一的實現(xiàn)類 PhoneWindow。不多說,來看看實現(xiàn)類 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();
}
}
當我們沒有調(diào)用 setContentView() 時,mContentParent (是ViewGroup) 是 null ,所以有兩行代碼值得我們關(guān)注 installDecor() 和 mLayoutInflater.inflate(layoutResID, mContentParent)
首先 mContentParent 作為第二個參數(shù)傳入了 inflate 方法中, 也就是說 我的布局中的 RelativeLayout 被層層解析之后的 View 視圖樹 作為了 mContentParent 的子 View 插入。
現(xiàn)在不知道 mContentParent 是什么沒關(guān)系,繼續(xù)跟進 installDecor() 方法。
隨著API level的升高,源碼發(fā)生了很多有關(guān) Feature 、 Style 和 Wiget 的細微變化,還是蠻有意思的
這里我還想說一句,相信在 Android 設(shè)計之初 PhoneWindow 這個類就存在了,顯然現(xiàn)在的這個命名有些問題,畢竟目前的設(shè)備不僅僅是 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設(shè)置
mDecorContentParent.setUiOptions(mUiOptions);
}
} else {
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
}
}
省略了與分析無關(guān)的代碼,其中很多是對 feature 和 style 屬性的一些判斷和設(shè)置,首先 installDecor() 方法從字面意思看,很有可能是初始化加載 DecorView 的,首先看看 PhoneWindow 中兩個成員變量 mDecor 和 mContentParent 分別是什么:
描述的信息可以概括為 mDector 是 窗體的頂級視圖,mContentParent 是放置窗體內(nèi)容的容器,也就是我們 setContentView() 時,所加入的 View 視圖樹。
當二者為 null 時,有兩行代碼值得關(guān)注,分別為 mDecor = generateDecor() 和 mContentParent = generateLayout(mDecor)
不過在此之前,先來看看這行尋找 decorContentParent 布局的代碼
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
decor_content_parent 看起來很眼熟的樣子,點擊它進入布局來看看:
為什么說 decor_content_parent 眼熟呢?打開布局查看器來看看
在 Hierarchy Viewer 中可以看到 ActionBarOverlayLayout 的布局文件的 id 正是 decor_content_parent 不光如此 布局文件中的每個 View 節(jié)點的名稱和 id 都與 Hierarchy Viewer 視圖中的一一對應(yīng)。再看其中的 FrameLayout 的 id 為 content , 我們自然而然的猜測它就是我們根布局 RelativeLayout 的父布局,心里一下有了底,繼續(xù)研究~
跟進 generateDecor() 方法:
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();
// 省略大量無關(guān)代碼
// 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");
}
// 省略無關(guān)代碼
mDecor.finishChanging();
// 返回 contentParent 并賦值給成員變量 mContentParent
return contentParent;
}
這個方法的代碼有300多行,剔除了很多無關(guān)代碼,我們分模塊來看:
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
首先 layoutResource 是系統(tǒng)的 xml 布局文件的 id,里面有我們設(shè)置窗體的 features 和 style 屬性,然后通過 decor.addView 添加進 mDector 視圖。這里也是我們要在 setContentView() 之前執(zhí)行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;
關(guān)鍵點來了, ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
通過 findViewById 找到系統(tǒng)修飾布局文件中 id 為:
這個 id 是不是非常眼熟,與我們上文的猜測不謀而合,這就是我們一直在尋找的作為 activity_main 的父布局的 FrameLayout
我們在布局文件查看器中再找一下:
return contentParent 這一步就返回了我們的成員變量 mContentParent
到現(xiàn)在為止其實整個知識點主干的邏輯已經(jīng)走完了,為大家花了一張簡單的思維導(dǎo)圖
并不復(fù)雜,線性邏輯調(diào)用還是蠻清晰的。
不過相信你也許會問,上文你僅僅提到了兩個布局呀,一個頂層的 DecorView 和 我們布局文件的父布局 FrameLayout ,而查看布局層級時,為什么有這么多其他這么多額外的布局呢?
因為隨著 Android API level 的不斷變化,組件也在隨之增多,比如 ActionBar Toolbar 等等,這些組件相關(guān)的布局是否加載與你的 feature 設(shè)置設(shè)備的特性相關(guān)聯(lián),而且版本不同,布局文件的層級結(jié)構(gòu)也在不斷變化著豐富著,我這個是 API22 的源碼,我做了一些對比,有許多代碼細節(jié)是不一樣的,比如在這里的 feature 就新增了 Toolbar ,但是大體上的邏輯框架肯定不會變
比如我們目前的 MainActivity 的視圖主要有兩大分支,一條設(shè)置 Toolbar 的相關(guān)配置,一條就是我們的 RelativeLayout 了。
寫在后面:
寫這篇博客的原因一是我自己要研究梳理總結(jié)這個知識點,二是想讓大家明白,Android 版本之間的迭代很快,一年前的博客闡述的觀點到今天可能就再不適用了,但是 PhoneWindow 管理布局視圖的這套邏輯框架,卻一直沒怎么改變。通過閱讀源碼,可以學(xué)習(xí) Google 工程師們良好的代碼風(fēng)格,汲取他們搭建框架的思想,讓我們自己寫的代碼也能如此健壯。
PS: PhoneWindow 什么時候能改個名字??!