[Android]Activity/Window/View的關(guān)系以及View的繪制時(shí)機(jī)

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í)行的操作:

  1. 獲取待啟動(dòng)的Activity組件信息。
  2. 通過(guò)Instrumentation的newActivity方法使用類加載器創(chuàng)建Activity對(duì)象。
  3. 嘗試創(chuàng)建Application對(duì)象(如果Application已經(jīng)創(chuàng)建,則不會(huì)重復(fù)創(chuàng)建)。
  4. 創(chuàng)建ContextImpl對(duì)象,并通過(guò)Activity的attach方法來(lái)完成一些重要數(shù)據(jù)的初始化(包括讓Activity跟Window關(guān)聯(lián))
  5. 調(diào)用Activity的onCreate方法。

Activity其他生命周期的調(diào)用都是通過(guò)Binder向AMS發(fā)請(qǐng)求,然后執(zhí)行的PIC操作,最后從ApplicationThread對(duì)生命周期調(diào)用。

層級(jí)關(guān)系:


View的層級(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)系。

setContentView是把view設(shè)置到window上去
attach()方法中的getWindow獲取的是PhoneWindow對(duì)象

這個(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方法。


PhoneWindow的setContentView方法

這個(gè)mContentParent是一個(gè)ViewGroup,「window的內(nèi)容放置在這個(gè)viewGroup里。它既是mDecor本身,也是mDecor的孩子(孩子存放著內(nèi)容)」:


mContentParent

所以,PhoneWindow里面包含了一個(gè)ViewGroup,setContentView其實(shí)就是將layout設(shè)置到了這個(gè)ViewGroup上了。

看上面的setContentView里,如果這個(gè)Window里的容器是空的,就installDecor。

PhoneWindow#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)。

DecorView extends FrameLayout,是window的top-level view

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ò)程中,ViewRootImplDecorView和WMS會(huì)彼此關(guān)聯(lián),至于詳細(xì)過(guò)程這里不展開(kāi)來(lái)說(shuō)了。

最后通過(guò)WMS調(diào)用ViewRootImpl#performTraverals方法開(kāi)始View的測(cè)量、布局、繪制流程。

這一部分總結(jié):

  1. WindowManagerServiceDecor添加到Window上了。
  2. WMS調(diào)用ViewRootImpl#performTraverals方法開(kāi)始View的測(cè)量、布局、繪制流程。

[番外篇]: Android只在UI主線程修改UI,是個(gè)謊言嗎?

先看一下,為什么這段代碼能完美運(yùn)行

子線程setImageResource

我運(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)用ViewRootImplsetview方法才開(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í)行,ViewRootImplcheckThread也沒(méi)有執(zhí)行。
也就是說(shuō)其實(shí)并不是完全不能在子線程里操作,各種帶UI的操作系統(tǒng)都只是盡量規(guī)避你這么做,因?yàn)檫@么做不好。

ViewRootImpl#checkThread

只要在創(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)始的,所以在onCreatesetImageResource的時(shí)候,ViewRoot沒(méi)有初始化整個(gè)view tree,ImageViewmAttachInfo是空的。


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的方案:


這兩段Style,上面那段是優(yōu)化前的,下面那段是優(yōu)化后的

以前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)畫。


指定入場(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ō)下后面的步驟:
獲取WindowManagerViewManager 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í)間,是從ThemeView繪制完成的時(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書人閱讀 175,263評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,543評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,323評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 54,874評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,095評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,551評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,720評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,358評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,612評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,330評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,690評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容