0x00 簡(jiǎn)介
本文沒(méi)有介紹WMS
調(diào)用ViewRootImpl#performTraverals
方法開(kāi)始的View的測(cè)量、布局、繪制流程(可參考View的工作原理),而是介紹了View開(kāi)始measure/layout/draw之前的一些流程,Activity、Window、View的關(guān)系,以及View的繪制時(shí)機(jī)。另外,分析了一個(gè)在子線程中也能更新View
的場(chǎng)景。
先簡(jiǎn)單介紹Activity啟動(dòng)。
從startActivity開(kāi)始 -> 調(diào)用Instrumentation -> Instrumentation通過(guò)Binder向AMS(ActivityManagerService)發(fā)請(qǐng)求,啟動(dòng)Activity。
啟動(dòng)Activity執(zhí)行的操作:
- 獲取待啟動(dòng)的Activity組件信息。
- 通過(guò)Instrumentation的newActivity方法使用類加載器創(chuàng)建Activity對(duì)象。
- 嘗試創(chuàng)建Application對(duì)象(如果Application已經(jīng)創(chuàng)建,則不會(huì)重復(fù)創(chuàng)建)。
- 創(chuàng)建ContextImpl對(duì)象,并通過(guò)Activity的attach方法來(lái)完成一些重要數(shù)據(jù)的初始化(包括讓Activity跟Window關(guān)聯(lián))
- 調(diào)用Activity的onCreate方法。
Activity其他生命周期的調(diào)用都是通過(guò)Binder向AMS發(fā)請(qǐng)求,然后執(zhí)行的PIC操作,最后從ApplicationThread對(duì)生命周期調(diào)用。
層級(jí)關(guān)系:
0x01 Window Attach到Activity上
Window如何跟Activity關(guān)聯(lián)?
每一個(gè)Activity都包含了唯一一個(gè)PhoneWindow,這個(gè)就是Activity根Window(之所以是說(shuō)根Window是因?yàn)樵谒厦婵梢栽黾痈嗥渌腤indow,例如:彈出框(dialog))
setContentView
方法的的實(shí)現(xiàn),其實(shí)是getWindow().setContentView(),也就是設(shè)置到Window上的。下面是Activity.java的源碼,里面可以看到Activity跟Window、View的關(guān)系。
這個(gè)PhoneView的代碼片段,是在Activity類的attach()方法里的。
在 attach 的時(shí)候執(zhí)行了PhoneWindow的初始化。
提到了 activity 的 attach 方法,該方法是在執(zhí)行Activity啟動(dòng)時(shí)在ActivityThread里面的performLaunchActivity調(diào)用的。performLaunchActivity里面做了很多Activity啟動(dòng)過(guò)程具體的操作,例如:主題、記錄Activity棧、執(zhí)行Activity onCreate 方法等。
Window是一個(gè)抽象類,PhoneWindow繼承Window。下面是PhoneWindow的setContentView方法。
這個(gè)mContentParent是一個(gè)ViewGroup,「window的內(nèi)容放置在這個(gè)viewGroup里。它既是mDecor本身,也是mDecor的孩子(孩子存放著內(nèi)容)」:
所以,PhoneWindow里面包含了一個(gè)ViewGroup,setContentView其實(shí)就是將layout設(shè)置到了這個(gè)ViewGroup上了。
看上面的setContentView里,如果這個(gè)Window里的容器是空的,就installDecor。
這個(gè)方法很長(zhǎng),可以看到mDecor如果是空,就generateDecor,如下。
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
DecorView是PhoneWindow類的一個(gè)內(nèi)部類,繼承于FrameLayout。
如果decor不為空,那么調(diào)用mDecor.setWindow(this),把decor和PhoneWindow關(guān)聯(lián)起來(lái)。
mContentParent是install在DecorView上的(我以為mContentParent包含了DecorView,其實(shí)不是)。
至此,我們理解了Window是Activity和View的中間人。
上面的過(guò)程,可以簡(jiǎn)單提煉下:
1. Activity#getWindow().setContentView()
2. Activity#attach() : mWindow = new PhoneWindow()
3. PhoneWindow#getContentView(),
4. if(mContentParent == null) installDecor();
WindowManager控制View:
Window對(duì)View的操作是通過(guò)WindowManager來(lái)處理的。WindowManager提供在Window上添加View、移除View、更新View的操作。
然而可見(jiàn) WindowManager 其實(shí)只是一個(gè)接口,真正的實(shí)現(xiàn)類是WindowManagerImpl
以addView為例,里面有點(diǎn)繞,直接忽略中間過(guò)程,最后執(zhí)行addView的是通過(guò)ViewRootImpl完成Window的添加工作的,它執(zhí)行了View的requestLayout方法,在requestLayout方法里會(huì)通過(guò)WindowSession完成Window的添加過(guò)程WindowSession是IWindowSession類型的,它是一個(gè)Binder對(duì)象,因此Window的添加工作其實(shí)是一次IPC調(diào)用。
到目前為止,通過(guò)setContentView方法,創(chuàng)建了DecorView和加載了我們提供的布局,但是這時(shí),我們的View還是不可見(jiàn)的,因?yàn)槲覀儍H僅是加載了布局,并沒(méi)有對(duì)View進(jìn)行任何的測(cè)量、布局、繪制工作。在View進(jìn)行測(cè)量流程之前,還要進(jìn)行一個(gè)步驟,那就是把DecorView添加至window中,然后經(jīng)過(guò)一系列過(guò)程觸發(fā)ViewRootImpl#performTraversals方法,在該方法內(nèi)部會(huì)正式開(kāi)始測(cè)量、布局、繪制這三大流程。
0x02 將DecorView添加至Window
每一個(gè)Activity組件都有一個(gè)關(guān)聯(lián)的Window對(duì)象,用來(lái)描述一個(gè)應(yīng)用程序窗口。
每一個(gè)應(yīng)用程序窗口內(nèi)部又包含有一個(gè)View對(duì)象,用來(lái)描述應(yīng)用程序窗口的視圖。
上文分析了創(chuàng)建DecorView的過(guò)程,現(xiàn)在則要把DecorView添加到Window對(duì)象中。
而要了解這個(gè)過(guò)程,我們首先要簡(jiǎn)單先了解一下Activity的創(chuàng)建過(guò)程:
首先,在ActivityThread#handleLaunchActivity中啟動(dòng)Activity,在這里面會(huì)調(diào)用到Activity#onCreate
方法,從而完成上面所述的DecorView創(chuàng)建動(dòng)作,當(dāng)onCreate()方法執(zhí)行完畢,在handleLaunchActivity方法會(huì)繼續(xù)調(diào)用到ActivityThread#handleResumeActivity
方法,我們看看這個(gè)方法的源碼:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
//...
ActivityClientRecord r = performResumeActivity(token, clearHide); // 這里會(huì)調(diào)用到onResume()方法
if (r != null) {
final Activity a = r.activity;
//...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow(); // 獲得window對(duì)象
View decor = r.window.getDecorView(); // 獲得DecorView對(duì)象
decor.setVisibility(View.INVISIBLE);//注意是INVISIBLE
ViewManager wm = a.getWindowManager(); // 獲得windowManager對(duì)象
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l); // 調(diào)用addView方法
}
//...
}
}
}
也就是說(shuō),在onResume
的時(shí)候,會(huì)獲取該activity所關(guān)聯(lián)的window
對(duì)象,DecorView
對(duì)象,以及windowManager
對(duì)象。
WindowManager
是抽象類,它的實(shí)現(xiàn)類是WindowManagerImpl
,所以后面調(diào)用的是WindowManagerImpl#addView
方法。
WindowManager
會(huì)調(diào)用ViewRootImpl#setView
方法,并把DecorView
作為參數(shù)傳遞進(jìn)去。在這個(gè)方法內(nèi)部,會(huì)通過(guò)跨進(jìn)程的方式向WMS(WindowManagerService)發(fā)起一個(gè)調(diào)用,從而將DecorView最終添加到Window上,在這個(gè)過(guò)程中,ViewRootImpl
、DecorView
和WMS會(huì)彼此關(guān)聯(lián),至于詳細(xì)過(guò)程這里不展開(kāi)來(lái)說(shuō)了。
最后通過(guò)WMS調(diào)用ViewRootImpl#performTraverals
方法開(kāi)始View的測(cè)量、布局、繪制流程。
這一部分總結(jié):
-
WindowManagerService
將Decor
添加到Window
上了。 - WMS調(diào)用
ViewRootImpl#performTraverals
方法開(kāi)始View的測(cè)量、布局、繪制流程。
[番外篇]: Android只在UI主線程修改UI,是個(gè)謊言嗎?
先看一下,為什么這段代碼能完美運(yùn)行?
我運(yùn)行了一下,確實(shí)可以更新圖片。
但我沒(méi)有嘗試,如果在onCreate()
里面設(shè)置一個(gè)按鈕,在點(diǎn)的時(shí)候再new Thread().start()
會(huì)怎樣。。我猜是會(huì)報(bào)錯(cuò)的。
為什么子線程也可以更新圖片?
根據(jù)前文我們可以得知,Activity
是在onResume
執(zhí)行之后,才將自身所在的Window添加到WindowManager
中的,然后才會(huì)調(diào)用ViewRootImpl
的setview
方法才開(kāi)始View繪制的,在繪制過(guò)程中才會(huì)檢查是否在UI線程中
上面的代碼之所以能運(yùn)行,是因?yàn)?code>ImageView還沒(méi)有完全初始化,mAttachInfo
是空的:
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
//....
p.invalidateChild(this, damage);
}
所以括號(hào)里的invalidate
也沒(méi)有執(zhí)行,ViewRootImpl
的checkThread
也沒(méi)有執(zhí)行。
也就是說(shuō)其實(shí)并不是完全不能在子線程里操作,各種帶UI的操作系統(tǒng)都只是盡量規(guī)避你這么做,因?yàn)檫@么做不好。
只要在創(chuàng)建Window/View/ViewRootView的線程更新UI,就是合法的,并不一定在UI線程。
知乎原帖的評(píng)論里有個(gè)大V說(shuō),這不就是Handler嗎?
我感覺(jué)這個(gè)問(wèn)題跟Handler
還是不太一樣的。Handler
是在子線程處理復(fù)雜信息,處理完了通過(guò)MessageQueue
或者post
來(lái)拋回到原來(lái)的thread,是跨線程通信;View的繪制過(guò)程是在主線程的。他想表達(dá)的可能是,可以用view.post
(實(shí)際上利用的是Handler
)來(lái)實(shí)現(xiàn)在主線程更新View。
其實(shí)看前面的總結(jié):
最后通過(guò)
WMS
調(diào)用ViewRootImpl#performTraverals
方法開(kāi)始View
的測(cè)量、布局、繪制流程。
View
的測(cè)量、布局、繪制流程是在ActivityThread
調(diào)用handleResumeActivity
之后,把decorView
加入到window
,把window
add到windowmanager
之后才開(kāi)始的,所以在onCreate
里setImageResource
的時(shí)候,ViewRoot
沒(méi)有初始化整個(gè)view tree,ImageView
的mAttachInfo
是空的。
14 December 2017
[番外篇-2] 優(yōu)化SplashActivity的啟動(dòng)速度
今天很高興又遇到一個(gè)跟上面的番外1以及0x03都有聯(lián)系的問(wèn)題,也是我們App中一直存在的問(wèn)題,就是進(jìn)入Splash的時(shí)間過(guò)長(zhǎng)的問(wèn)題。為什么,之前錢包采用的是透明Activity的方案:
以前SplashActivity用的是上面那段Style,其實(shí)我一直非常愧疚,也一直嘗試想要改變它,因?yàn)樗膯?wèn)題在于,我們的Application啟動(dòng)的過(guò)程很長(zhǎng),導(dǎo)致冷啟動(dòng)狀態(tài)下點(diǎn)擊應(yīng)用圖標(biāo)到進(jìn)入SplashActivity的時(shí)間,會(huì)有1.5秒之久,在性能差的手機(jī)上甚至更長(zhǎng)。怎么辦呢?雖然把Application中的初始化操作大量地都放到子線程/延后了(其實(shí)應(yīng)該還有優(yōu)化空間),但是仍然難以避免,所以我們采取的策略就是上面那段代碼,把背景設(shè)成透明,這樣導(dǎo)致一個(gè)非常不好的體驗(yàn),點(diǎn)擊APP后很久才會(huì)展示Splash,這回讓用戶覺(jué)得自己沒(méi)點(diǎn)到,甚至把鍋推到Android系統(tǒng)身上,我感到非常難過(guò),因?yàn)锳ndroid系統(tǒng)沒(méi)那么差啊。
我之前就想到一個(gè)辦法,就是下面那個(gè)Style,把透明色去掉,用Splash的placeholder圖;同時(shí)一定要把windowAnimationStyle設(shè)置成null。這也是很多App都廣泛采用的折衷方案,比如Bilibili,當(dāng)時(shí)我在知乎上私信他們的一個(gè)開(kāi)發(fā),他告訴我去搜索冷啟動(dòng)白屏這樣的關(guān)鍵字。
但是在我們的App上還有一個(gè)問(wèn)題,就是我發(fā)現(xiàn)我們的App會(huì)「閃」一下(動(dòng)畫的樣子,在不同的機(jī)器上不一樣),在有些手機(jī)上表現(xiàn)得是「黑」一下。
解決閃一下的動(dòng)畫問(wèn)題
我用控制變量法發(fā)現(xiàn),「閃一下」是由于我們的SplashActivity啟動(dòng)過(guò)程中會(huì)有一個(gè)透明的PermissionActivity去請(qǐng)求權(quán)限。那我就懷疑,應(yīng)該是PermissionActivity的動(dòng)畫造成的。但是我去看了下,PermissionActivity的主題動(dòng)畫也是上面那樣寫的null。咨詢新來(lái)的同事,他讓我在Acitivty的finish后面加一個(gè):
overridePendingTransition(0, 0);
看了下,是手動(dòng)指定入場(chǎng)和出場(chǎng)動(dòng)畫。
設(shè)置上之后,動(dòng)畫確實(shí)消失了。注意,這個(gè)要寫在finish的后面,寫在面的話會(huì)無(wú)效,可能是因?yàn)閒inish會(huì)覆蓋掉原來(lái)設(shè)置的動(dòng)畫。
解決黑一下的問(wèn)題(關(guān)鍵!?。。。。?!)
但是在一些性能差的手機(jī)上,仍然存在「黑」一下的問(wèn)題。
一句話總結(jié):onCreate執(zhí)行的時(shí)候view并沒(méi)有展示出來(lái),onResume中WMS調(diào)用ViewRootImpl#performTraverals才會(huì)展示出來(lái),所以需要在view.post里面去調(diào)用permissionActivity,否則會(huì)顯示黑色(黑色是什么,有可能是App的背景)。
其實(shí)這個(gè)問(wèn)題就跟上面的番外1以及0x03都有聯(lián)系了。說(shuō)下原因和解決方法。
原因:
0x03里提到,
ActivityThread#handleLaunchActivity
會(huì)去launch Activity
調(diào)用Activity#onCreate
,完成DecorView
的創(chuàng)建動(dòng)作
然后handleLaunchActivity()
會(huì)繼續(xù)直接ActivityThread#handleResumeActivity
方法(這個(gè)方法很關(guān)鍵)
讓我們讀一下源碼:
首先會(huì)執(zhí)行performResumeActivity
去調(diào)用onResume();
然后會(huì)get一些列的東西,getWindow
獲取window
,然后用window#getDecorView
獲取decor
;
然后decor.setVisibility(View.INVISIBLE)
;
說(shuō)下后面的步驟:
獲取WindowManager
:ViewManager wm = a.getWindowManager()
WindowManager
會(huì)調(diào)用ViewRootImpl#setView
方法,并把DecorView
作為參數(shù)傳遞進(jìn)去。
在這個(gè)方法內(nèi)部,會(huì)通過(guò)跨進(jìn)程的方式向WMS
(WindowManagerService)發(fā)起一個(gè)調(diào)用,從而將DecorView
最終添加到Window
上。
最后通過(guò)WMS
調(diào)用ViewRootImpl#performTraverals
方法開(kāi)始View的測(cè)量、布局、繪制流程。
最終,ImageView
顯示出來(lái)。
所以,之前我在onCreate()
里,立刻去調(diào)用PermissionActivity,造成的問(wèn)題跟上面的番外1一樣,都是在View
沒(méi)有加載完就做別的事情了。這里黑一下是因?yàn)椋?code>ImageView還沒(méi)有繪制完成,但是SplashActivity是透明的,那段黑色的時(shí)間,是從Theme
到View
繪制完成的時(shí)間的間隔。
解決的辦法,imgView.post(new Runnable());里執(zhí)行PermissionActivity的操作。
但是,黑色是什么顏色?PermissionActivity的底下難道不是正在繪制的SplashActivity嗎?所以我猜想,在從Theme的背景顯示到Activity繪制完成的時(shí)間里,如果有別的Activity覆蓋在上面,底下的Activity的Theme會(huì)被覆蓋,所以會(huì)呈現(xiàn)黑色。
Ref:
http://www.lxweimin.com/p/5297e307a688
http://blog.csdn.net/a553181867/article/details/51477040