<b>個(gè)人博客地址: <a >https://kehanchenk.github.io/</a></b>
<h2>前言</h2>
說起 setContentView,大家肯定不陌生,從名字上理解就能知道它設(shè)置內(nèi)容的視圖,很多人覺得現(xiàn)在網(wǎng)上關(guān)于它的資料很多,但是為什么我還是要重復(fù)制造輪子呢?,這里我想說的是,自己花時(shí)間去整理的內(nèi)容,期間你學(xué)到的內(nèi)容是很多的,而且站在更高的基礎(chǔ)上也會(huì)有更多的收獲,比如 AppCompatActivity 的流程分析,現(xiàn)在 as 使用的都是AppCompatActivity,你不應(yīng)該去了解 AppCompatActivity ? 還有關(guān)于 Google 是如何去適配不同版本的。AppCompatActivity 和 Activity 的 UI 分析流程有什么不同? 整個(gè)過程中設(shè)置的主題,布局是如何被加載上去呢?這些都需要你自己親自去實(shí)踐才能明白 Google 工程師在背后所做的努力。
而以上的疑問我都在下文能一一解答,只要你用心看肯定是能收獲的,下面我從兩個(gè)方面去分析,先分析 Activity,然后是 AppCompatActivity 的 UI 繪制流程,期間我也會(huì)解答很多關(guān)于平時(shí)你遇到的問題,從 setContentView 走進(jìn) Framework 。
<h4>關(guān)于如何去閱讀源碼</h4>
如果有經(jīng)驗(yàn)的可以直接跳過,對(duì)于源碼閱讀有限的可以參考。
- 閱讀源碼一定要有線索,帶著問題去研究源碼(比如研究 setContentView 抓住主線程,對(duì)其他的暫時(shí)忽略,等研究完主線程,再消化個(gè)別細(xì)節(jié))
- 源碼報(bào)錯(cuò)?其實(shí)源碼內(nèi)容在,對(duì)閱讀研究源碼影響不大。
- 找不到類?學(xué)會(huì)使用搜索,比如本文 Phonewindow 類可能代碼跳轉(zhuǎn)無法進(jìn)入,試試雙擊 Shift 直接搜索?;蛘卟檎椅募?ctrl+shift+N ,查找類中方法快捷鍵:Ctrl+F12 。源碼太長(zhǎng)?找不到當(dāng)前方法?Ctrl+F12 搜索方法。如果沒有源碼直接去 SDk Manager 下載。
<h4> 一. 從setContentView開始,了解view的加載過程</h4>
setContentView 到底做了什么,為什么調(diào)用之后就能加載我們想要的布局文件?我們從Activity 的setContentView 開始, 而它通過 Window 類調(diào)取的方法,而 Window 是抽象類。最終調(diào)用的是PhoneWindow 這個(gè)實(shí)現(xiàn)類的 setContentView 方法。Phonewindow 又是如何來的呢?這個(gè)先劇透下,Activity 的啟動(dòng)過程中的 attach() 方法創(chuàng)建的。

其中 mContentParent 其實(shí)就是裝載界面最外層的 ViewGroup ,分析:首先如果當(dāng)前 Viewgroup 為空,則執(zhí)行installDecor();

分析:截取方法的前一部分,mDecor 是什么呢?其實(shí)就是感覺都見過的 DecorView (Google 對(duì)它的注釋是:它是 window 窗口的頂級(jí)視圖,同時(shí)包括widow 的裝飾。抽象不容易理解,下文就會(huì)揭開它到底是什么鬼?和我們的布局關(guān)系是什么)

DecorView 具體是作為什么存在的,且看分析:上述代碼的意思是當(dāng) mDecor 為空?qǐng)?zhí)行g(shù)enerateDecor。很多博客分析都是默認(rèn)為空,直接執(zhí)行。缺乏說服力。那他是否為空呢?
接著看代碼:

分析:其實(shí)在 PhoneWindow 的構(gòu)造方法中就新建了 <a name="decorview" >DecorView</a>,通過 getDecorView() 創(chuàng)建當(dāng)前 DecorView。我們繼續(xù)看下這個(gè)方法的實(shí)現(xiàn):

是否恍然大悟了?installDecor() 方法似曾相識(shí),就是上文的 installDecor(),所以其實(shí)在 PhoneWindow 的構(gòu)造方法最終執(zhí)行的是 installDecor() 方法,所以不管 mDecor 是否為 null,它都是執(zhí)行了該方法創(chuàng)建的 DecorView 的。 我們回到 installDecor() 中,如果 mDecor (DecorView) 是null,通過 generateDecor() 方法創(chuàng)建一個(gè) DecorView?!痉椒▓D示代碼在下文】當(dāng) DecorView 創(chuàng)建成功之后,接下來就是通過 generateLayout 創(chuàng)建 mContentParent。(mContentParent 是一個(gè) Viewgroup 對(duì)象)

分析:generateDecor() 方法相對(duì)而已邏輯很清晰,上述代碼只是判斷了 context 對(duì)象,最后返回值很暴力直接 new DecorView 返回結(jié)果

分析:generateLayout() 方法很長(zhǎng),我截取倆部分,上半部分首先它是通過獲取 windowstyle. 判斷布局是否是 dialog ,接下來是解析是否為 Activity 設(shè)置主題,標(biāo)題欄等,源碼這段代碼很長(zhǎng),可以通過查看源碼分析,很容易入手的代碼,在你們分析這段代碼中,注意這段代碼:

注:我們平時(shí)會(huì)遇到的問題,獲取 requestFeature 必須在 setContentView 之前設(shè)置。因?yàn)樵谶@里獲取本地配置的 LocalFeatures,所以你必須在之前設(shè)置 requestFeature,也就是 setContentView
繼續(xù)分析下半部分:

分析:這段代碼承接之前,是通過獲取本地配置的主題,或者 Activity 設(shè)置的屬性,通過這些屬性去針對(duì)性加載不同的 xml 資源。這就是我們 Activity 選擇不同的主題,界面顯示不一樣的原因。因?yàn)槊恳粋€(gè)主題都會(huì)加載不同的 xml 資源布局。當(dāng)通過 feature 選擇不同的主題之后接下來就是如何把我們自己寫的布局加載上。此處我取其中一個(gè)布局(R.layout.screen_title)作為例子分析。

分析:上圖同樣是 generateLayout 的后半部分,為什么需要單獨(dú)提出,就是因?yàn)檫@里解釋了 DecorView 為什么是頂級(jí)視圖。
上圖中通過id獲取到 viewGroup 對(duì)象 id 其實(shí)就是 com.android.internal.R.id.content,就是下圖xml中的 content 對(duì)象
<b>如果解釋 DecorView 作為頂級(jí)View 就是接下來的關(guān)鍵代碼:</b>
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
layoutResource 是我們作為例子的 xml 文件, DecorView 通過以上方法解析主題文件提供最上層的 xml 資源文件,并且將解析的 view執(zhí)行 addView() 方法作為最外層的View,而我們做的只是其實(shí)是將 提供的xml 文件內(nèi)容部分解析加載,也就是為什么方法名字叫做 setCOntentView 了。

分析:onResourcesLoaded 將最外層的布局通過 addView() ,其實(shí)就是添加到了 DecorView 中,而 DecorView 是如何如何被加載到屏幕上顯示出來。這個(gè)都是 Activity 啟動(dòng)的過程中的操作。接下來的文章我會(huì)一一解答

xml 是:screen_title.xml 就是上文我們作為例子講解的 layoutResource 的代碼。

分析:當(dāng)通過 features 確定使用某個(gè)具體的 xml 文件后,首先是 DecorView 先解析通過主題確定的xml資源文件,然后獲取xml資源文件中的 id 為 content 的FrameLayout 對(duì)象 (其實(shí)每一個(gè)不同的xml文件都會(huì)有一個(gè)相同 的id (R.id.content)的 FrameLayout )
最后其實(shí) generateLayout() 方法就是把這個(gè)找到的 id 為 content 的對(duì)象 return 了。并且這個(gè)返回值 Viewgroup 其實(shí)就已經(jīng)是 mContentParent 了。返回查看下對(duì) generateLayout 的引用的方法 installDecor 。 它是什么???一臉懵逼?? 哈哈。
想想我們分析這段代碼的最初是從哪里入手的,我們是從 setContentView 開始分析的源碼。現(xiàn)在回到 setCOntentView 中查看下, 最初從 installDecor() 分析下去,generateLayout() 方法返回的 mContentParent 對(duì)象,它其實(shí)就是我們最初方法里的 mContentParent 對(duì)象。 然后我們接著分析 setContentView(), 它在獲得 mContentParent 這個(gè) viewgroup 對(duì)象后。接下去是判斷了Activity 是否存在類似于轉(zhuǎn)場(chǎng)動(dòng)畫之類的效果,如果有則先執(zhí)行轉(zhuǎn)場(chǎng)動(dòng)畫,沒有則將我們分析的 mContentParent 對(duì)象(id 為 content 的 FrameLyout)。解析??匆韵麓a:
mLayoutInflater.inflate(layoutResID, mContentParent);//第一個(gè)參數(shù)就是我們提供的布局id
mLayoutInflater 布局解析器將我們通過 setContentView 傳進(jìn)去的 布局id 解析到 mContentParent 中。至此我們加載布局這一部分流程算正式告一段落。
<b>附:</b>
為了更好的理解這個(gè)這段代碼:再提供一個(gè)結(jié)構(gòu)圖提供思路參考:

分析:你會(huì)發(fā)現(xiàn)所有的我們所有的view的繪制都是從DecorView開始的,通過xml文件加載設(shè)定好的 actionbar
、title 和為我們提供自定義內(nèi)容的 Framelayout。
總結(jié):回顧這段過程,帶著問題去源碼中尋找答案,針對(duì)線索去跟蹤源碼。我們從 Activity 的 setContentView() 開始,找到 Window 類,但是因?yàn)?window 是抽象類, 通過注釋可以知道它有唯一的實(shí)現(xiàn)類 PhoneWindow ,我們查看實(shí)現(xiàn)類的方法 setContentView(),它通過 installDecor() 一步步創(chuàng)建DecorView,F(xiàn)rameLayout,到最后解析setContentView().整個(gè)過程其實(shí)并不復(fù)雜。
我們進(jìn)一步分析:其實(shí)每一個(gè) Activity 都有一個(gè)關(guān)聯(lián)的 Window 對(duì)象,用來呈現(xiàn),描述應(yīng)用程序窗口,每一個(gè) window 對(duì)象 又包含了一個(gè) DecorView 對(duì)象。而 DecorView 對(duì)象就是描述窗口的視圖-就是對(duì)應(yīng)的 xml 資源布局。 結(jié)構(gòu)關(guān)系理解:

Window 作為抽象類,提供通用的繪制窗口的 API。
Phonewindow 作為具體實(shí)現(xiàn)類,同時(shí)包含 DecorView 對(duì)象,它是所有窗口的根 view
<h3>二. AppCompatActivity的流程分析</h3>
終于分析完 Activity 的流程,接下來我們繼續(xù)分析,Google 工程師是如何做到只是替換一個(gè) Activity 就能做到支持 Material design,同時(shí)兼容之前所有版本呢?下面我同樣以最詳細(xì)的方式解釋源碼中的奧秘。
在我們現(xiàn)在使用 as開 發(fā),現(xiàn)在都是默認(rèn)使用 AppCompatActivity 而不是 Activity,所以這套流程對(duì)于 AppCompatActivity 有那么一點(diǎn)不適用,整體的過程肯定還是對(duì)的,只是 google 做了很多適配工作,AppCompatActivity 中 setContentView 方法是通過了代理

分析:在 AppCompatActivity 的 setContentView 是調(diào)用 getDelegate() 的,但是返回的 AppCompatDelegate 其實(shí)是抽象類,直接查找返回的 create() ,我們能知道它有著很多實(shí)現(xiàn)類。

分析: 可以看到 AppCompatDelegate 不同的 version 有著不同的版本,但是其實(shí)隨著版本提高他們的實(shí)現(xiàn)類其實(shí)是逐級(jí)實(shí)現(xiàn)的。 換句話說 AppCompatDelegateImplN extends AppCompatDelegateImplV23 ,而 AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 ,類推,隨著版本的提高去新的實(shí)現(xiàn)類去拓展,同時(shí)兼容低版本,這樣的設(shè)計(jì)其實(shí)就是符合 Android 向下兼容的特性的?;氐酱a中, getDelegate().setContentView() ,那他的實(shí)現(xiàn)類中去查線索,基于兼容,其實(shí)只在 AppCompatDelegateImplV9 中實(shí)現(xiàn)了 setContentView();

分析:AppCompatDelegateImplV9 的 setContentView 實(shí)現(xiàn) ,進(jìn)一步查看 ensureSubDecor() ,從下面代碼可以知道我們還需要查看 createSubDecor()。


分析:在這段代碼中,其實(shí)我們能發(fā)現(xiàn)和 Activity 分析是類似的,首先也是先獲取 AppCompatTheme 主題 ,不同的是由于兼容 material design 必須要實(shí)現(xiàn) AppCompatTheme 主題,代碼中也很明確如果獲取的屬性沒有 AppCompatTheme_windowActionBar 會(huì)拋出異常。這就是當(dāng)平時(shí)我們使用 AppCompatActivity 的時(shí)候但是沒有使用 windowNoTitle 屬性會(huì)報(bào)錯(cuò)的原因。接下來到了關(guān)鍵的這段代碼:
<pre >mWindow.getDecorView();</pre>
不知道是否還有印象,這個(gè)是在 Activity 的分析中出現(xiàn)過,<a href="#decorview">回到過去</a>,在之前的分析中我們知道它其實(shí)是調(diào)用 PhoneWindow,走的流程是Activity的流程,通過 installDecor() 方法 到 generateLayout() ,此處再?gòu)?qiáng)調(diào)下 generateLayout() 的返回值如上文分析,返回值是id為 content 的FrameLayout ,而現(xiàn)在其實(shí)還是同樣的會(huì)按照之前分析,DecorView 其實(shí)和之前沒有區(qū)別的。接著代碼下一部分分析。

分析:這段代碼是承接上圖的。此處重新定義新的 viewgroup 對(duì)象 subDecor,首先做了一個(gè)判斷 mWindowNoTitle,它是什么?其實(shí)對(duì)主題是否設(shè)置 Window.FEATURE_NO_TITLE,是則為 false,它的賦值是 我們上文中 createSubDecor() 這個(gè)方法的 requestWindowFeature(Window.FEATURE_NO_TITLE),這個(gè)方法判斷賦值為 true , 這段代碼是通過是否設(shè)置主題,是否支持 Actionbar 來判斷 subDecor 來加載對(duì)應(yīng)的布局,或是否設(shè)置 FEATURE_NO_TITLE 來顯示隱藏 title 內(nèi)容的。
接下里的分析 Google 如何做到一個(gè)巧妙的替換:

分析:上圖重要代碼我已經(jīng)標(biāo)注,首先 subDecor 我們新的 viewgroup,并且是已經(jīng)賦值相應(yīng)布局的。接下來的<pre >mWindow.findViewById(android.R.id.content);</pre>
他拿到的是 <b>mWindow.getDecorView();</b> 然后走的Activity分析的流程的返回的結(jié)果。接下來它判斷了 content 可能已將視圖添加到窗口的內(nèi)容視圖中,因此需要將其遷移到我們的內(nèi)容視圖中。然后才是重點(diǎn),原來的 content 的 id抹除,而把這邊 subDecor 的新主題中的內(nèi)容視圖的 id 改為了 content!然后把整個(gè) subDecor 都 setContentView 給了 window。而他的實(shí)現(xiàn)其實(shí)都是 Phonewindow。

分析:上圖的 setContentView 實(shí)現(xiàn),其實(shí)是執(zhí)行倆個(gè)參數(shù)的方法,前半部分因?yàn)?mContentParent 不為null,所以不會(huì)執(zhí)行 installDecor(), 最后把我們新建的 subDecor 的添加到了 mContentParent上。然后我們回到最初 AppCompatDelegateImplV9 的 setContentView() 方法中。

分析: mSubDecor 這個(gè)其實(shí)就是之前 mSubDecor = createSubDecor(); 返回的 Viewgroup ,就是我們上文的 subDecor. 那就是現(xiàn)在獲得的 content 就是變換過的,然后通過 LayoutInflater 去解析我們自己寫的布局到 content上。至此整個(gè) AppCompatActivity 的分析就結(jié)束了。也就是關(guān)系適配工作結(jié)束。剩下的就是渲染 xml 資源。
小結(jié):Google 為了適配,在不影響之前的版本,采用的方法是在基礎(chǔ)上重新加了一層,首先UI的分析 Activity 還是會(huì)執(zhí)行,然后如果是 AppCompatActivity 才會(huì)執(zhí)行獲取他特有的主題,進(jìn)一步獲取布局文件,將布局文件放在新的 Viewgroup 上,然后把之前的 id 做修改,把原本是 content 的賦值給新的Viewgroup ,然后之前的 id 為 content 直接 addview
如果不熟悉可以再看看上文的內(nèi)容,對(duì)照源碼學(xué)習(xí)會(huì)有意想不到的收獲。
<h3>一點(diǎn)點(diǎn)感悟</h3>
整個(gè)流程下來有沒有決定 Google 工程師其實(shí)在代碼的實(shí)現(xiàn)上也是很有想法的,或者說其實(shí)也是無奈之舉,由于Android 的開源性,造成了市場(chǎng)上其實(shí)各個(gè)版本參差不齊,而 android 勢(shì)必要發(fā)展,但是為了適配缺一直是難題,相對(duì)于ios的閉源環(huán)境,每一個(gè) ios 都再自己的控制范圍內(nèi),利于管理。因?yàn)殚_源成功,但是有得有失,隨著Android適配工作不斷進(jìn)行,代碼會(huì)越來越臃腫。這是不是 google 想研發(fā)新的系統(tǒng)的動(dòng)力呢?時(shí)間給出答案。
<h3>結(jié)語</h3>
此文作為博文開篇,分享給大家自己的理解,希望能幫助能幫助的人,同時(shí)也更加希望能給我提出文章的不足或者漏洞,以此共勉!