DEVLOG 10.21 Android UI體系知識&面試題
參考內(nèi)容:AndroidUI體系

問題:
在Activity中Window和View如何分工協(xié)作?
何時在Activity中獲取View寬高?
這篇文章主要是回答以上的問題,但是在回答以上的問題中也會出現(xiàn)一些子問題。弄清楚這些問題,有利于我們了解Window WindowManager Activity View的關(guān)系。

在Activity中Window和View如何分工協(xié)作?
我們知道ActivityThread相當(dāng)于是App的主函數(shù)類,Activity#onCreate的開始可以追溯到ActivityThread#handleLaunchActivity中:
handleLaunchActivity中首先初始化了WindowManagerGlobal,從名字可以看出,這個類一定和Window以及WindowManager是有關(guān)的,至于這個類如何和Window和WM產(chǎn)生關(guān)聯(lián),我們待會會總結(jié)。然后在handleLaunchActivity中,又調(diào)用了performLaunchActivity,在這個方法中會調(diào)用Activity#attach。
ActivityThread#performLaunchActivity
performActivity通過反射機制創(chuàng)建了Activity的實例,并且構(gòu)建了Application的實例,當(dāng)attach方法執(zhí)行完成之后,使用記錄當(dāng)前的Activity狀態(tài)是ON_CREATE。那么,不言而喻,attach方法中應(yīng)該會調(diào)用我們實現(xiàn)的Activity#onCreate回調(diào)。接著我們來看看Activity#attach方法:
Activity#attach
因為Activity的回調(diào)方法中通常我們使用setContentView加載當(dāng)前resId指定的布局,但是這個布局是在DecorView的ContentView中,而DecorView又在PhoneWindow中。所以當(dāng)前Activity#attach時需要創(chuàng)建PhoneWindow的實例,并且綁定PhoneWindow到WindowManager中。

繼續(xù)看ActivityThread#performLaunchActivity
當(dāng)Activity#attach執(zhí)行完成之后,performLaunchActivity繼續(xù)執(zhí)行,會執(zhí)行到這一行代碼:
在Instrumentation#callActivityOnCreate中會調(diào)用Activity#performCreate,這里面就回調(diào)用我們寫的onCreate回調(diào)。因此,一個不太標(biāo)準(zhǔn)的從ActivityThread#handlePerformLaunchActivity到Activity#onCreate的時序圖,如下圖所示:

雖然我們知道在Activity的生命周期方法調(diào)度完成時我們可以看到我們寫的布局文件,但是我們目前并不能看到Window和View的關(guān)系,于是我們可以猜想,既然onCreate方法中沒有,那么,Window加載View的代碼可能會在onResume上。這是因為在官方文檔中也說明,onResume是程序到前臺的標(biāo)志。
ActivityThread#handleResumeActivity
ActivityThread#handleResumeActivity方法首先調(diào)用performResumeActivity,在performResumeActivity完成之后在會調(diào)用WindowManager#addView向Window中添加布局。
所以,實際上布局是在onResume完成之后才被加載在PhoneWindow中的,不過具體的內(nèi)容我們還是需要看看performResumeActivity:
ActivityThread#performResumeActivity
套路和前面的都差不多,在ActivityThread中執(zhí)行Activity#performResume然后轉(zhuǎn)到啟動相關(guān)類Instrumentation#callActivityOnResume,在執(zhí)行onResume回調(diào)。

因此我們可以做一個小小的總結(jié),關(guān)于Activity中Window和View,他們之間的合作關(guān)系和Activity的生命周期是密切相關(guān)的:
onCreate階段:

但是在onCreate中,并不會把View加載到PhoneWindow中,這個說來也非常好理解,畢竟我們在回調(diào)中才解析布局文件xml,怎么會在PhoneWindow創(chuàng)建之前addView呢?
onResume階段:

ViewRootImpl如何成為Window和View的橋梁?
剛才我們看到WindowManager可以將DecorView加載到PhoneWindow中,這個過程還可以仔細(xì)地分析一番。在分析之前我們先總結(jié)一下Window相關(guān)的類之間的關(guān)系:

ViewManager只是一個接口,定義了基本的對于View的添加和刪除工作
WindowManager也是一個接口,但是我們通常操作的都是這個類,他的實現(xiàn)類是WindowManageImpl。WindowManagerImpl又通過將職責(zé)委托給WindowManagerGlobal實現(xiàn),之所以使用這么復(fù)雜的【套娃】邏輯,好處有兩點:
這是一種外觀模式,我們通過WindowManager就可以操作WindowManagerGlobal和framework層通信。
WindowManagerGlobal也是一個全局單例。根據(jù)上面的代碼分析,創(chuàng)建Activity就回創(chuàng)建對應(yīng)的WindowManager和PhoneWindow,但是這些所有的WindowManager都基于WindowManagerGlobal,節(jié)省內(nèi)存。
回到問題【ViewRootImpl如何成為Window和View的橋梁?】本身,我們需要查看一下WindowManager#addView的代碼:
在WindowManagerGlobal中初始化了ViewRootImpl,然后調(diào)用了setView。跟蹤ViewRootImpl#setView可以發(fā)現(xiàn)這個方法最后會調(diào)用WindowSession#addToDispaly,再調(diào)用WindowManagerService#addWindow,整體的調(diào)用鏈如圖:

所以可以看到ViewRootImpl確實充當(dāng)了一個橋梁,上面抓住了WindowManager,下面連接的是WindowSession(是IWindowSession.Stub的實現(xiàn)類,是Binder機制的一部分)。
如何onResume中獲取View的寬高?
這個問題可以轉(zhuǎn)換成另外的一個子問題,View#measure是在什么時候執(zhí)行的?
View#measure的執(zhí)行時機
上面已經(jīng)說過個, onResume會先于WindowManager#addView,所以回調(diào)函數(shù)本身會在測量之前執(zhí)行,這樣在onResume中嘗試獲取寬高一定會返回0,更不用說在onCreate中。
解決問題的思路:
handler.post( Runnable {}, delay):這里不可以不加delay。如果不加delay,這個消息在消息隊列中還是會先執(zhí)行,而我們的目的是想等到onResume執(zhí)行完成,WindowManager#addView之后再拿到View寬高,這個大小設(shè)置為100ms。
View.post(Runnable {}): View.post的原理就是將當(dāng)前的Runnable放入了HandlerActionQueue的數(shù)組中,然后在ViewRootImpl#performTraversals中執(zhí)行。ViewRootImpl#performTraversals是執(zhí)行測量 布局 繪制的開始,肯定也會在WindowManager#addView之后
3. ?onWindowFocusChanged回調(diào):當(dāng)失去焦點時會被調(diào)用
4. addOnGlobalLayoutListener:當(dāng)ViewTree變化的時候會被調(diào)用。
在子線程中能不能更新UI?
這個例子中代碼會報錯嗎?
其實并不會,因為在這里,textView被setContentView加載之后設(shè)置setText時只是將String變量存儲到TextView中,此時TextView并沒有開始performTraversals,不會檢查UI線程,所以沒有問題。