[Android]Activity/Window/View的關系以及View的繪制時機

0x00 簡介

本文沒有介紹WMS調用ViewRootImpl#performTraverals方法開始的View的測量、布局、繪制流程(可參考View的工作原理),而是介紹了View開始measure/layout/draw之前的一些流程,Activity、Window、View的關系,以及View的繪制時機。另外,分析了一個在子線程中也能更新View的場景。

先簡單介紹Activity啟動。

從startActivity開始 -> 調用Instrumentation -> Instrumentation通過Binder向AMS(ActivityManagerService)發請求,啟動Activity。

啟動Activity執行的操作:

  1. 獲取待啟動的Activity組件信息
  2. 通過Instrumentation的newActivity方法使用類加載器創建Activity對象
  3. 嘗試創建Application對象(如果Application已經創建,則不會重復創建)。
  4. 創建ContextImpl對象,并通過Activity的attach方法來完成一些重要數據的初始化(包括讓Activity跟Window關聯)
  5. 調用Activity的onCreate方法。

Activity其他生命周期的調用都是通過Binder向AMS發請求,然后執行的PIC操作,最后從ApplicationThread對生命周期調用

層級關系:


View的層級關系

0x01 Window Attach到Activity上

Window如何跟Activity關聯?
每一個Activity都包含了唯一一個PhoneWindow,這個就是Activity根Window(之所以是說根Window是因為在它上面可以增加更多其他的Window,例如:彈出框(dialog))

setContentView方法的的實現,其實是getWindow().setContentView(),也就是設置到Window上的。下面是Activity.java的源碼,里面可以看到Activity跟Window、View的關系。

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

這個PhoneView的代碼片段,是在Activity類的attach()方法里的。

在 attach 的時候執行了PhoneWindow的初始化。
提到了 activity 的 attach 方法,該方法是在執行Activity啟動時在ActivityThread里面的performLaunchActivity調用的。performLaunchActivity里面做了很多Activity啟動過程具體的操作,例如:主題、記錄Activity棧、執行Activity onCreate 方法等。

Window是一個抽象類,PhoneWindow繼承Window。下面是PhoneWindow的setContentView方法。


PhoneWindow的setContentView方法

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


mContentParent

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

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

PhoneWindow#installDecor()

這個方法很長,可以看到mDecor如果是空,就generateDecor,如下。

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

DecorView是PhoneWindow類的一個內部類,繼承于FrameLayout。
如果decor不為空,那么調用mDecor.setWindow(this),把decor和PhoneWindow關聯起來。

DecorView extends FrameLayout,是window的top-level view

mContentParent是install在DecorView上的(我以為mContentParent包含了DecorView,其實不是)。
至此,我們理解了Window是Activity和View的中間人。
上面的過程,可以簡單提煉下:

1. Activity#getWindow().setContentView() 
2. Activity#attach() : mWindow = new PhoneWindow()
3. PhoneWindow#getContentView(), 
4. if(mContentParent == null) installDecor();

WindowManager控制View:

Window對View的操作是通過WindowManager來處理的。WindowManager提供在Window上添加View、移除View、更新View的操作。
然而可見 WindowManager 其實只是一個接口,真正的實現類是WindowManagerImpl
以addView為例,里面有點繞,直接忽略中間過程,最后執行addView的是通過ViewRootImpl完成Window的添加工作的,它執行了View的requestLayout方法,在requestLayout方法里會通過WindowSession完成Window的添加過程WindowSession是IWindowSession類型的,它是一個Binder對象,因此Window的添加工作其實是一次IPC調用。

到目前為止,通過setContentView方法,創建了DecorView和加載了我們提供的布局,但是這時,我們的View還是不可見的,因為我們僅僅是加載了布局,并沒有對View進行任何的測量、布局、繪制工作。在View進行測量流程之前,還要進行一個步驟,那就是把DecorView添加至window中,然后經過一系列過程觸發ViewRootImpl#performTraversals方法,在該方法內部會正式開始測量、布局、繪制這三大流程。

0x02 將DecorView添加至Window

每一個Activity組件都有一個關聯的Window對象,用來描述一個應用程序窗口。
每一個應用程序窗口內部又包含有一個View對象,用來描述應用程序窗口的視圖。
上文分析了創建DecorView的過程,現在則要把DecorView添加到Window對象中。
而要了解這個過程,我們首先要簡單先了解一下Activity的創建過程:

首先,在ActivityThread#handleLaunchActivity中啟動Activity,在這里面會調用到Activity#onCreate方法,從而完成上面所述的DecorView創建動作,當onCreate()方法執行完畢,在handleLaunchActivity方法會繼續調用到ActivityThread#handleResumeActivity方法,我們看看這個方法的源碼:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { 
    //...
    ActivityClientRecord r = performResumeActivity(token, clearHide); // 這里會調用到onResume()方法

    if (r != null) {
        final Activity a = r.activity;

        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow(); // 獲得window對象
            View decor = r.window.getDecorView(); // 獲得DecorView對象
            decor.setVisibility(View.INVISIBLE);//注意是INVISIBLE
            ViewManager wm = a.getWindowManager(); // 獲得windowManager對象
            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); // 調用addView方法
            }
            //...
        }
    }
}

也就是說,在onResume的時候,會獲取該activity所關聯的window對象,DecorView對象,以及windowManager對象。

WindowManager是抽象類,它的實現類是WindowManagerImpl,所以后面調用的是WindowManagerImpl#addView方法。

WindowManager會調用ViewRootImpl#setView方法,并把DecorView作為參數傳遞進去。在這個方法內部,會通過跨進程的方式向WMS(WindowManagerService)發起一個調用,從而將DecorView最終添加到Window上,在這個過程中,ViewRootImplDecorView和WMS會彼此關聯,至于詳細過程這里不展開來說了。

最后通過WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。

這一部分總結:

  1. WindowManagerServiceDecor添加到Window上了。
  2. WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。

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

先看一下,為什么這段代碼能完美運行

子線程setImageResource

我運行了一下,確實可以更新圖片。
但我沒有嘗試,如果在onCreate()里面設置一個按鈕,在點的時候再new Thread().start()會怎樣。。我猜是會報錯的。
為什么子線程也可以更新圖片?

根據前文我們可以得知,Activity是在onResume執行之后,才將自身所在的Window添加到WindowManager中的,然后才會調用ViewRootImplsetview方法才開始View繪制的,在繪制過程中才會檢查是否在UI線程中

上面的代碼之所以能運行,是因為ImageView還沒有完全初始化,mAttachInfo是空的:

final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
    //....
    p.invalidateChild(this, damage);
}

所以括號里的invalidate也沒有執行,ViewRootImplcheckThread也沒有執行。
也就是說其實并不是完全不能在子線程里操作,各種帶UI的操作系統都只是盡量規避你這么做,因為這么做不好。

ViewRootImpl#checkThread

只要在創建Window/View/ViewRootView的線程更新UI,就是合法的,并不一定在UI線程。
知乎原帖的評論里有個大V說,這不就是Handler嗎?
我感覺這個問題跟Handler還是不太一樣的。Handler是在子線程處理復雜信息,處理完了通過MessageQueue或者post來拋回到原來的thread,是跨線程通信;View的繪制過程是在主線程的。他想表達的可能是,可以用view.post(實際上利用的是Handler)來實現在主線程更新View。

其實看前面的總結:

最后通過WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。

View的測量、布局、繪制流程是在ActivityThread調用handleResumeActivity之后,把decorView加入到window,把window add到windowmanager之后才開始的,所以在onCreatesetImageResource的時候,ViewRoot沒有初始化整個view tree,ImageViewmAttachInfo是空的。


14 December 2017

[番外篇-2] 優化SplashActivity的啟動速度

今天很高興又遇到一個跟上面的番外1以及0x03都有聯系的問題,也是我們App中一直存在的問題,就是進入Splash的時間過長的問題。為什么,之前錢包采用的是透明Activity的方案:


這兩段Style,上面那段是優化前的,下面那段是優化后的

以前SplashActivity用的是上面那段Style,其實我一直非常愧疚,也一直嘗試想要改變它,因為它的問題在于,我們的Application啟動的過程很長,導致冷啟動狀態下點擊應用圖標到進入SplashActivity的時間,會有1.5秒之久,在性能差的手機上甚至更長。怎么辦呢?雖然把Application中的初始化操作大量地都放到子線程/延后了(其實應該還有優化空間),但是仍然難以避免,所以我們采取的策略就是上面那段代碼,把背景設成透明,這樣導致一個非常不好的體驗,點擊APP后很久才會展示Splash,這回讓用戶覺得自己沒點到,甚至把鍋推到Android系統身上,我感到非常難過,因為Android系統沒那么差啊。

我之前就想到一個辦法,就是下面那個Style,把透明色去掉,用Splash的placeholder圖;同時一定要把windowAnimationStyle設置成null。這也是很多App都廣泛采用的折衷方案,比如Bilibili,當時我在知乎上私信他們的一個開發,他告訴我去搜索冷啟動白屏這樣的關鍵字。

但是在我們的App上還有一個問題,就是我發現我們的App會「閃」一下(動畫的樣子,在不同的機器上不一樣),在有些手機上表現得是「黑」一下。

解決閃一下的動畫問題

我用控制變量法發現,「閃一下」是由于我們的SplashActivity啟動過程中會有一個透明的PermissionActivity去請求權限。那我就懷疑,應該是PermissionActivity的動畫造成的。但是我去看了下,PermissionActivity的主題動畫也是上面那樣寫的null。咨詢新來的同事,他讓我在Acitivty的finish后面加一個:

overridePendingTransition(0, 0);

看了下,是手動指定入場和出場動畫。


指定入場和出場動畫

設置上之后,動畫確實消失了。注意,這個要寫在finish的后面,寫在面的話會無效,可能是因為finish會覆蓋掉原來設置的動畫。

解決黑一下的問題(關鍵!!!!!!!)

但是在一些性能差的手機上,仍然存在「黑」一下的問題。
一句話總結:onCreate執行的時候view并沒有展示出來,onResume中WMS調用ViewRootImpl#performTraverals才會展示出來,所以需要在view.post里面去調用permissionActivity,否則會顯示黑色(黑色是什么,有可能是App的背景)。

其實這個問題就跟上面的番外1以及0x03都有聯系了。說下原因和解決方法。
原因:
0x03里提到,
ActivityThread#handleLaunchActivity會去launch Activity
調用Activity#onCreate,完成DecorView的創建動作
然后handleLaunchActivity()會繼續直接ActivityThread#handleResumeActivity方法(這個方法很關鍵)

讓我們讀一下源碼:
首先會執行performResumeActivity去調用onResume();
然后會get一些列的東西,getWindow獲取window,然后用window#getDecorView獲取decor
然后decor.setVisibility(View.INVISIBLE);

說下后面的步驟:
獲取WindowManagerViewManager wm = a.getWindowManager()
WindowManager會調用ViewRootImpl#setView方法,并把DecorView作為參數傳遞進去。
在這個方法內部,會通過跨進程的方式向WMS(WindowManagerService)發起一個調用,從而將DecorView最終添加到Window
最后通過WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。

最終,ImageView顯示出來。
所以,之前我在onCreate()里,立刻去調用PermissionActivity,造成的問題跟上面的番外1一樣,都是在View沒有加載完就做別的事情了。這里黑一下是因為,ImageView還沒有繪制完成,但是SplashActivity是透明的,那段黑色的時間,是從ThemeView繪制完成的時間的間隔。
解決的辦法,imgView.post(new Runnable());里執行PermissionActivity的操作。
但是,黑色是什么顏色?PermissionActivity的底下難道不是正在繪制的SplashActivity嗎?所以我猜想,在從Theme的背景顯示到Activity繪制完成的時間里,如果有別的Activity覆蓋在上面,底下的Activity的Theme會被覆蓋,所以會呈現黑色。


Ref:
http://www.lxweimin.com/p/5297e307a688
http://blog.csdn.net/a553181867/article/details/51477040

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容