最近想要理清我們的View是如何加載到界面中的,最好的方式就是分析源代碼,這里一同分享給有需要的朋友們。內容較多,需要一定的耐心,請斟酌學習!
我們都知道,在開發Android應用程序時,經常會在Activity的onCreate方法里調用setContentView方法,將布局文件或者View對象傳入,但是很多人并沒有去分析后續是如何加載到面并顯示出來的,接下來就順藤摸瓜將其摘下來,查看的是Android 7.1源碼。
1、從setContentView 方法開始摸索
就簡單從HelloWorld工程的onCreate方法開始吧:
查看其父類Activity的setContentView方法,代碼如下:
可以看出這里先得到一個 Window 對象,然后調用 Window 對象的setContentView方法。
這樣分析Activity中的 setContentView 方法可以看到,界面繪制并不是由 Activity 完成的,是調用了 Window 類的 setContentView 來實現的。
所以我們繼續查看 Window 類的代碼:
發現Window 類其實是一個抽象類,且 setContentView 是一個抽象方法。所以其具體實現是由 Window類的實現類來完成的(后面我們會知道該實現類是PhoneWindow)。
2、分析Window類的實現類
為了找出Window類的具體實現類,回到Activity的setContentView方法,然后進入getWindow方法:
getWindow 方法很簡單,只是返回一個Window對象,那么Window對象到底是在哪兒實例化的呢?接著我們繼續尋找Window對象的實例化代碼,最終確認在Activity類的attach方法(attach的調用后續再分析),查看attach方法的源代碼:
由此可見上面的Window對象就是PhoneWindow對象,所以我們從Activity的setContentView方法定位到了PhoneWindow的setContentView方法。
PhoneWindow的構造方法非常簡單,就是獲取了LayoutInflater對象,為了后續加載xml布局只有文件:
3、繼續分析PhoneWindow類的setContentView方法
繼續進入PhoneWindow類,查看setContentView方法源代碼:
從源碼可以知道,這里主要包括三個步驟:
如果父容器為空則初始化父容器,否則移除所有子視圖;
調用LayoutInflater類的inflate方法將xml布局文件加載到父容器;
回調Callback通知ContentView發生改變,其中的Callback可能由Activity實現。
后面兩步比較簡單,這里主要來看第一步的父容器初始化流程,進入PhoneWindow類的installDecor方法:
這個方法的代碼有點兒長,這里只截取重要部分,主要操作包括三部分:
調用generateDecor()創建出mDecor,即DecorView對象;
generateLayout(mDecor)傳入mDecor對象,生成mContentParent ;
設置標題欄信息。
首先查看generateDecor方法,源代碼如下:
這個方法非常簡單,就是創建了一個DecorView對象,并返回出去。關于DecorView的具體內容可以查看其構造方法:
這里也是比較簡單的,從這里知道了DecorView是一個FrameLayout。DecorView是Activity的頂級View,一般來說它內部包含標題欄和內容欄(加載布局文件layout.xml,即mContentParent)。內容欄是一定存在的,并且具體的id是‘content’。因此這個時候創建出的DecorView還是一個空白的FrameLayout。先不要急,這是怎么知道的,后面會繼續分析。
繼續回到installDecor方法中調用的generateLayout方法:
這個方法代碼非常多,我們只需要關注重點即可。
首先獲取Application android:theme=/, Activity/節點指定的themes或者代碼;
然后獲取窗口Features, 設置相應的修飾布局文件,這些xml文件位于frameworks/base/core/res/res/layout下;
接著調用了DecorView的onResourcesLoaded方法將上面選定的布局文件inflate為View,添加到DecorView中;
找到id為content的framlayout賦給mContentParent,由于已經將屏幕View加為mDecor的子View,因此mContentParent也是mDecor的子View;
設置mDecor的背景和標題。
這里我們先隨便找一個布局文件,如screen_simple.xml:
就會驚奇的發現此LinearLayout就是Activity的界面,由兩部分組成 ActionBar + content。從布局文件就可以認證上述所說的content,源碼中id為@android:id/content的FrameLayout就是內容區域,其會賦值給PhoneWindow類中的屬性mContentParent,分析后續代碼后再來細看。
回到generateLayout方法,查看調用的onResourcesLoaded方法:
主要就是將適配的布局文件加載進來生成root視圖,調用addView方法添加到DecorView視圖。
繼續回到generateLayout方法,將窗口修飾布局文件中id=@android:id/content的View賦值給mContentParent, 后續自定義的view和layout都將是其子View。處理完成后就將mContentParent返回。
再回到PhoneWindow的setContentView方法中, 繼續調用了mLayoutInflater.inflate(layoutResID, mContentParent),在這里就是把我們寫的布局文件通過inflater加入到mContentParent中。這樣我們寫的布局文件成功的添加到DecorView中的mContentParent。
現在只是完成了DecorView的創建并初始化,我們還需要把這個創建并初始化完DecorView添加并顯示到屏幕上,這里我們就需要用到WindowManager。
4、Activity入口
很多同學會有疑問,上面的attach方法是在哪里調用的?然后View又是如何顯示出來的?我們知道,Activity的入口就是ActivityThread類,我們找到其中的handleMessage處理的代碼:
這里的代碼非常多,但是仔細去查看,會發現很多有用的消息,會根據不同的message調用對應的方法,如handleLaunchActivity()、handleResumeActivity()、handlePauseActivity()、handleStopActivity()等,從方法名就能大概猜出來起用途。這里我們來分別查看handleLaunchActivity()和handleResumeActivity()方法,其他方法類似。
5、performLaunchActivity
首先來看handleLaunchActivity方法,源碼如下:
主要調用了performLaunchActivity方法,繼續查看performLaunchActivity源代碼:
這里通過Activity的類名構建一個Activity對象,可以查看Instrumentation類的newActivity方法:
繼續回到performLaunchActivity的源代碼:
這里是不是看到了非常熟悉的方法,就是我們前面看到的Activity類的attach()方法。就是在attach方法里面初始化PhoneWindow對象的。
后面調用了Instrumentation類的callActivityOnCreate方法,源代碼如下:
這里主要通過Instrumentation對象執行Activity的onCreate()方法,Activity的生命周期方法都是由Instrumentation對象來調用的,這里不再詳細深入。
到目前為止,View只是加載到了Activity,并沒有顯示出來,繼續研究ActivityThread的handleResumeActivity方法。
6、performResumeActivity
首先來看handleResumeActivity方法:
這里首先調用了performResumeActivity方法,查看performResumeActivity源代碼:
繼續調用了Activity的performResume方法,繼續深入源代碼:
可以看到仍然是通過Instrumentation類調用了Activity的onResume()方法。
然后回到handleResumeActivity方法,找到下面的wm.addView()方法:
這個方法非常關鍵,wm是上面a.getWindowManager()獲取到的mWindowManger對象,而這個對象是WindowManagerImpl。
7、addView
繼續進入WindowManagerImpl類的addView方法:
其實WindowManagerImpl類的方法大部分都是代理的WindowManagerGlobal的方法。繼續進入WindowManagerGlobal類的addView方法:
從上面的代碼可以看出,addView方法中,創建了一個ViewRootImpl對象,然后調用ViewRootImpl.setView()方法,繼續查看setView()方法。
該方法首先將傳進來的參數view賦值給mView,mView將是這個對象所認識的root節點,也是整個Activity的root的節點,即DecorView。
接著調用了requestLayout()方法,首次調度執行 layout,這里會觸發 onAttachToWindow 和 創建 Surface方法。深入查看ViewRootImpl中requestLayout()方法:
該方法首先檢查了是否在主線程,然后就執行了scheduleTraversals()方法。
這里需要注意的就是Runnable對象,繼續往后看:
這個Runnable的run()方法中,調用了doTraversal()方法:
可以看到doTraversal()方法又調用了performTraversals()方法:
這個方法非常長,內部邏輯也很復雜,但是主體邏輯很清晰。其執行的過程可簡單的概括為:是否需要重新計算視圖的大小(measure)、是否需要重新布局視圖的位置(layout),以及是否需要重繪(Draw)。也就是我們常說的View的繪制流程,由于這里涉及的內容實在太多,關于View的繪制后續再分享。
回到ViewRootImpl類的setView()方法,繼續查看源碼:
從這里可以看到view的父親注冊為自己,于是mDecor知道了自己父親是誰,即整個Activity設置了一個根節點,在此之前調用setContentView()只是將自己的layout布局add到PhoneWindow.mContentParent,但是mDecor并不知道自己的parent是誰,現在整個view的樹形結構中有了根節點,也就是ViewRootImpl,那么requestLayout()就有效了,就可以進行后面的measure、layout、draw三步操作了。
總結
那么最后再來一個精簡的總結,加深理解。
用戶在Activity中調用setContentView,然后調用Window的setContentView,這時會檢查DecorView是否存在,如果不存在則創建DecorView對象,然后把我們自己的 View 添加到 DecorView 中。
這里可以用一個Activity層次關系圖來表示,會更加直觀清晰。
如果把以上這些流程梳理通透了,那么在開發中可以為我們節省不少時間了,也便于一些框架設計,也可以方便在系統的理解上實現出來多種定制任務。而且在一些中高級開發面試的時候,也會經常被問及到這方面的內容。如果還有疑問的童鞋,歡迎留言繼續討論。
今天就先分享到這里,后續將推出更多精彩內容,歡迎一起探討學習進步。
此文章版權為微信公眾號分享達人秀(ShareExpert)——鑫鱻所有,若轉載請備注出處,特此聲明!