????????Android開發者都知道要想建立一個頁面,最普遍常見的做法就是新建一個Activity,并且在res/layout中新建一個Layout布局,然后Activity繼承自Activity或者AppCompatActivity之后重寫onCreate方法,最后使用setContentView(resId)讓這個Activity和布局id為resId的布局產生聯系,這樣跳轉到這個Activity的時候就可以顯示resId的布局了。
? ? ? ? 那么大家是否想過了,為什么setContentView()這個方法就可以將一個xml格式的布局文件顯示在手機屏幕上?在它的后面系統究竟做了什么操作?
? ? ? ? 可以看看setContentView()的源碼是這樣的(注意:這個是基于Android api26的情況下)
? ? ? ? 由于該activity是繼承AppCompatActivity,所以在AppCompatActivity下面的setContentView實現有三個。相信大家也能看到,AppCompatActivity不直接操作view,而是通過一個叫AppCompatDelegate的類進行view的操作。其中getDelegate()方法是這樣的
那么,問題就來了,這個mDelegate這個AppCompatDelegate類是干什么的呢?有什么作用?好,繼續跟蹤下去,探索AppCompatDelegate.create方法
注意,create方法是定義在AppCompatDelegate這個類下面的靜態方法,一般普通的activity都會調用最上面的那個create(activity,? activity.getWindow(), callback)方法,本質來說最終都會調用最下面的需要判斷Build.VERSION.SDK_INT版本的create方法。那么其中的這些個AppCompatDelegateImplXX的關系是這樣的
AppCompatDelegateImplN? ?extends? ?AppCompatDelegateImplV23
AppCompatDelegateImplV23 extends? AppCompatDelegateImplV14
AppCompatDelegateImplV14 extends? AppCompatDelegateImplV11
AppCompatDelegateImplV11 extends? AppCompatDelegateImplV9
AppCompatDelegateImplV9? ?extends? AppCompatDelegateImplBase
那么這個一步步繼承過來到最后的AppCompatDelegateImplBase又是什么呢?會發現,這個AppCompatDelegateImplBase是繼承AppCompateDelegate的。那么回過頭來看看AppCompatDelegate這個類,首先它是個抽象類,其次這個類里面定義了很多activity生命周期的抽象方法,如下:
????????看到這里,可能大家心里差不多明白了,這個AppCompatDelegate更像是個代理的作用,所有不同版本的activity的生命周期方法也好、setContentView等方法也好,都可以通過繼承AppCompatDelegate這個類產生不同的操作。比如,在api是26的手機上,進行setContentView實際上是AppCompatDelegate的子類AppCompatImplN的setContentView方法;那么同理在api是10的手機上,進行setContentView實際上是AppCompatDelegate的子類AppCompatDelegateImplV9的setContentView方法。從Java語言角度來看,這是多態和繼承的典型表現。AppCompatDelegate也是官方實現夜間模式最好的工具。
? ? ? ? 繼續往下走,在眾多的AppCompatDelegateImplBase的實現類中,除了AppCompatDelegateImplV9這個實現類以外,發現均沒有重寫setContentView這個方法,那么最終activity中的setContentView經過一系列的輾轉,最終是在這里面實現的。
????????可以小結一下,在繼承自AppCompatActivity的activity中,setContentView方法在系統根據不同的api版本找到AppCompatDelegate的對應版本的實現子類,經過一系列的繼承,最終會在AppCompatDelegateImplV9中進行實現。
? ? ? ? 那么開始分析setContentView方法中僅僅只有五行的代碼:
? 第一個是ensureSubDecor這個方法。這個方法在AppCompatDelegateImplV9中可以找到
????????mSubDecorInstalled是個boolean類型的變量,這個變量是用來標識window sub-decor layout布局是否初始化的,在mSubDecor初始化后會發現mSubDecorInstalled會被賦值為true。那么變量mSubDecor是什么東西呢?往前找到定義變量的地方,會發現
private ViewGroup mSubDecor;
那么,這個mSubDecor是個ViewGroup,好,接下來來看createSubDecor方法,看看這個ViewGroup類型的mSubDecor是如何創建出來的。
createSubDecor方法比較長,進行分段分析:
可以看到,在最開始,是先獲取了AppCompatTheme屬性的TypedArray,然后我們會找到一個經常出現的一個異常
"You need to use a Theme.AppCompat theme (or descendant) with this activity."
也就是經常說的使用了AppCompatActivity卻沒有指定Theme.AppCompat主題。
其中,最關鍵的一句話:mWindow.getDecorView()。這句話放這里是什么意思呢?通過注釋大概了解到是要確保Window已經初始化了該Window的decor。
那么先來研究一下mWindow.getDecorView這個方法。首先,這個mWindow是個全局變量,那么它在哪里初始化賦值的呢?我們通過跟蹤,會發現mWindow這個對象是父類AppCompatDelegateImplBase中的一個Window類型變量,賦值實在父類AppCompatDelegateImplBase的構造方法中賦值的。
那么繼續找下去,會在AppCompatDelegate中的create方法中找到,如下圖:
那么繼續跟蹤,activity.getWindow又是什么東西呢?接下去會發現mWindow對象是定義在Activity里面的一個全局變量,mWindow賦值是在Activity的attach方法中賦值的。
Activity方法attach這個是涉及到了Activity的啟動流程,它是在啟動一個Activity過程中由android.app.ActivityThread.performLaunchActivity()這個方法調用的,暫時不去深究。
????????Window是Android里面的一個抽象類,而PhoneWindow是Window的唯一的實現類。去繼續研究PhoneWindow類,如果有無法打開PhoneWindow這個源碼的情況,可以找到本地文件下的android.jar包,復制到Android studio里面的libs目錄下,添加為依賴包,就可以打開PhoneWindow源碼了。
PhoneWindow的構造方法
????????構造方法中很重要的一個全局變量,mDecor,這個是DecorView類的實例。那么mDecor = (Decor) preservedWindow.getDecorView()這個方法是給DecorView類型進行賦值的方法。我們經常說的Android最底層的布局是DecorView,那么實際上DecorView是一個繼承自FrameLayout的自定義布局。那么如何mDecor是如何初始化的呢?
繼續看getDecorView這個方法,這個方法很簡單,就是判斷mDecor為null的話就執行installDecor方法
那么類型為DecorView的實例mDecor是通過generateDecor方法去初始化的
簡而言之,就是在這個方法里面new了一個DecorView對象,賦值給mDector。
還有個重要的變量,mContentParent這個,是ViewGroup的實例,是通過generateLayout方法進行初始化的,注意,generateLayout是需要傳入剛剛初始化好的mDecor對象的。
著重看下contentParent的初始化
有沒有發現很熟悉,findViewById方法,里面的ID_ANDROID_CONTENT實際上就是com.android.internal.R.id.content。所以PhoneWindow里面的mContentParent實際上是通過findViewById找到控件id為content而來的。
再回過頭來看,之前所說的在AppCompatDelegateImplV9里面的createSubDecor方法里面的ViewGroup類型mSubDecor是如何初始化的呢?
繼續來看createSubDecor方法下半部分
由于根據主題的設定不一樣,這里面的subDecor有不同的賦值,不僅僅只是包括上圖幾項。
繼續看下去,最關鍵是是mWindow.setContentView(subDecor);
那么在之前大段篇幅講的是mWindow對象是什么,是從哪里來的,在哪里定義,在哪里初始化,現在,在這個地方,如果已經明白了mWindow對象的來龍去脈,那么這里就不難理解,我們看下mWindow.setContentView方法。由于Window唯一抽象類是PhoneWindow,那么需要去PhoneWindow里面去找setContentView方法
????????不知道大家還記得,mContentParent是什么?它是一個ViewGroup對象,并且是通過findViewById找到id為content的控件來的。那么我們在AppCompatDelegateImplV9里面的setContentView里面的ensureSubDecor里面的createSubDector方法里面,上半部分已經通過mWindow.getDector方法來進行generateDector和generateLayout的初始化,即mDector和mContentParent已經準備好了,那么在AppCompatDelegateImplV9里面的setContentView里面的ensureSubDecor里面的createSubDector方法里面下半部分的mWindow.setContentView,最后直接進行mContentParent.addView方法,將AppCompatDelegateImplV9里面辛苦創建出來的ViewGroup類型subDecor添加到PhoneWindow對象里面的父容器mContentParent里面去了。
? ? ? ? 那么自此,最主要最復雜的ensureSubDecor方法已經完成了。
接下去的就好理解了,同樣也是從android.R.id.content這個控件id找到父容器contentParent,按照PhoneWindow中的generateLayoutf方法里面來分析,這里的contentParent和PhoneWindow里面的mContentParent指向的是同一個控件。
LayoutInflate.from(mContext).inflate(resId, contentParent);
在類LayoutInflate中找到
當然,如果我們最開始的Activity不是繼承自AppCompatActivity的話,而且繼承Activity,那么上述的分析流程是否還是成立的?
答案是肯定的,可以看到
getWindow()方法是返回當前的Window對象,即mWindow。那么作為Window唯一的實現類PhoneWindow,getWindow().setContentView在PhoneWindow中的方法又回到了setContentView方法里面,所以實際上是和AppCompatDelegateImplV9里面的createSubDecor里面的mWindow.setContentView一樣的。Google在推出AppcompatActivity肯定是考慮過與以前版本的Activity兼容的,本質上是相通的。
總結一下,在AppCompatActivity中,系統會創建一個類型為ViewGroup的mSubDecor對象,該對象是需要根據主題屬性inflate成一個ViewGroup對象(包含是否是懸浮的、是否有ActionBar、是不是OverlayActionMode等等),最終是需要用PhoneWindow對象mWindow調用setContentView方法,將該具有AppCompat主題屬性的mSubDecor當作參數傳過去,添加到由PhoneWindow通過findViewById方法找到id為content的控件mContentParent調用addView方法,將mSubDecor添加到根ViewGroup即mContentParent中去。
?那么相對的,如果是在Activity中,則會簡單很多,不會有AppCompatDelegate對象,直接會調用mWindow的setContentView方法,殊途同歸。不過需要注意的一點是,如果直接調用PhoneWindow里面的setContentView(int resId),那么布局文件的解析工作是需要在這里進行的;如果是在AppCompatDelegate的createSubDecor方法調用mWindow.setContentView(View view),那么在PhoneWindow里面僅僅只是將mSubDecor添加到mContentParent里面而已,布局的解析還是需要在AppCompatDelegateImplV9里面的setContentView里面完成的,可以對比一下
思考:之前在PhoneWindow類里面大量出現的DecorView實例mDecorView和PhoneWindow里面的ViewGroup類型實例mContentParent的關系是什么呢?這個可以通過PhoneWindow中的方法generateLayout找到答案。
其中有個int類型的
layoutResource對象,發現在各個判斷中都有賦值,那么我們隨便選個layout去看看
不管是哪個布局,其中肯定會定義一個FrameLayout,并且id固定為"content",那么解析去的mDecor.startChanging和mDecor.onResourceLoaded(mLayoutInflater, layoutResource)大家肯定也才猜想得出來,作用就是去解析layoutResource的布局,并且添加到mDecor中。
因為DecorView本身就是個FrameLayout,所以自然而然的可以使用addView方法
到這一步,我們心中大概很清楚,mDecor是Activity的底層View,其中有個固定id為content的控件(實際上都是FrameLayout),PhoneWindow可以通過findViewById直接獲取到該FrameLayout,例如PhoneWindow里面的mContentParent就是這么來的,然后我們所有的在AppCompatActivity里面也好,還是本身就在Activity里面也好,所有控件的添加、移除等都是通過mContentParent來進行控制操作的。
那么自此,setContentView方法已經差不多研究完了。其實還遺留下一個問題,那么就是mDecorView這個代表是一個FrameLayout的對象,它在Activity被加載進來了,那么它是如何顯示在屏幕上的呢?需要借助于ActivityThread的performResumeActivity方法。需要使用WindowManager對象調用addView方法。