本文原創,轉載請注明出處。
歡迎關注我的 簡書 ,關注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質量的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 ,所以我們直接去看 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 ,進而調用了 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 中兩個成員變量 mDecor 和 mContentParent 分別是什么:
描述的信息可以概括為 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 看起來很眼熟的樣子,點擊它進入布局來看看:
為什么說 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);
}
這個沒什么可多說的,就是為我們的窗體 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
我們在布局文件查看器中再找一下:
return contentParent 這一步就返回了我們的成員變量 mContentParent
到現在為止其實整個知識點主干的邏輯已經走完了,為大家花了一張簡單的思維導圖
并不復雜,線性邏輯調用還是蠻清晰的。
不過相信你也許會問,上文你僅僅提到了兩個布局呀,一個頂層的 DecorView 和 我們布局文件的父布局 FrameLayout ,而查看布局層級時,為什么有這么多其他這么多額外的布局呢?
因為隨著 Android API level 的不斷變化,組件也在隨之增多,比如 ActionBar Toolbar 等等,這些組件相關的布局是否加載與你的 feature 設置設備的特性相關聯,而且版本不同,布局文件的層級結構也在不斷變化著豐富著,我這個是 API22 的源碼,我做了一些對比,有許多代碼細節是不一樣的,比如在這里的 feature 就新增了 Toolbar ,但是大體上的邏輯框架肯定不會變
比如我們目前的 MainActivity 的視圖主要有兩大分支,一條設置 Toolbar 的相關配置,一條就是我們的 RelativeLayout 了。
寫在后面:
寫這篇博客的原因一是我自己要研究梳理總結這個知識點,二是想讓大家明白,Android 版本之間的迭代很快,一年前的博客闡述的觀點到今天可能就再不適用了,但是 PhoneWindow 管理布局視圖的這套邏輯框架,卻一直沒怎么改變。通過閱讀源碼,可以學習 Google 工程師們良好的代碼風格,汲取他們搭建框架的思想,讓我們自己寫的代碼也能如此健壯。
PS: PhoneWindow 什么時候能改個名字啊!