0x00 簡介
本文沒有介紹WMS
調用ViewRootImpl#performTraverals
方法開始的View的測量、布局、繪制流程(可參考View的工作原理),而是介紹了View開始measure/layout/draw之前的一些流程,Activity、Window、View的關系,以及View的繪制時機。另外,分析了一個在子線程中也能更新View
的場景。
先簡單介紹Activity啟動。
從startActivity開始 -> 調用Instrumentation -> Instrumentation通過Binder向AMS(ActivityManagerService)發請求,啟動Activity。
啟動Activity執行的操作:
- 獲取待啟動的Activity組件信息。
- 通過Instrumentation的newActivity方法使用類加載器創建Activity對象。
- 嘗試創建Application對象(如果Application已經創建,則不會重復創建)。
- 創建ContextImpl對象,并通過Activity的attach方法來完成一些重要數據的初始化(包括讓Activity跟Window關聯)
- 調用Activity的onCreate方法。
Activity其他生命周期的調用都是通過Binder向AMS發請求,然后執行的PIC操作,最后從ApplicationThread對生命周期調用。
層級關系:
0x01 Window Attach到Activity上
Window如何跟Activity關聯?
每一個Activity都包含了唯一一個PhoneWindow,這個就是Activity根Window(之所以是說根Window是因為在它上面可以增加更多其他的Window,例如:彈出框(dialog))
setContentView
方法的的實現,其實是getWindow().setContentView(),也就是設置到Window上的。下面是Activity.java的源碼,里面可以看到Activity跟Window、View的關系。
這個PhoneView的代碼片段,是在Activity類的attach()方法里的。
在 attach 的時候執行了PhoneWindow的初始化。
提到了 activity 的 attach 方法,該方法是在執行Activity啟動時在ActivityThread里面的performLaunchActivity調用的。performLaunchActivity里面做了很多Activity啟動過程具體的操作,例如:主題、記錄Activity棧、執行Activity onCreate 方法等。
Window是一個抽象類,PhoneWindow繼承Window。下面是PhoneWindow的setContentView方法。
這個mContentParent是一個ViewGroup,「window的內容放置在這個viewGroup里。它既是mDecor本身,也是mDecor的孩子(孩子存放著內容)」:
所以,PhoneWindow里面包含了一個ViewGroup,setContentView其實就是將layout設置到了這個ViewGroup上了。
看上面的setContentView里,如果這個Window里的容器是空的,就installDecor。
這個方法很長,可以看到mDecor如果是空,就generateDecor,如下。
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
DecorView是PhoneWindow類的一個內部類,繼承于FrameLayout。
如果decor不為空,那么調用mDecor.setWindow(this),把decor和PhoneWindow關聯起來。
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上,在這個過程中,ViewRootImpl
、DecorView
和WMS會彼此關聯,至于詳細過程這里不展開來說了。
最后通過WMS調用ViewRootImpl#performTraverals
方法開始View的測量、布局、繪制流程。
這一部分總結:
-
WindowManagerService
將Decor
添加到Window
上了。 - WMS調用
ViewRootImpl#performTraverals
方法開始View的測量、布局、繪制流程。
[番外篇]: Android只在UI主線程修改UI,是個謊言嗎?
先看一下,為什么這段代碼能完美運行?
我運行了一下,確實可以更新圖片。
但我沒有嘗試,如果在onCreate()
里面設置一個按鈕,在點的時候再new Thread().start()
會怎樣。。我猜是會報錯的。
為什么子線程也可以更新圖片?
根據前文我們可以得知,Activity
是在onResume
執行之后,才將自身所在的Window添加到WindowManager
中的,然后才會調用ViewRootImpl
的setview
方法才開始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
也沒有執行,ViewRootImpl
的checkThread
也沒有執行。
也就是說其實并不是完全不能在子線程里操作,各種帶UI的操作系統都只是盡量規避你這么做,因為這么做不好。
只要在創建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
之后才開始的,所以在onCreate
里setImageResource
的時候,ViewRoot
沒有初始化整個view tree,ImageView
的mAttachInfo
是空的。
14 December 2017
[番外篇-2] 優化SplashActivity的啟動速度
今天很高興又遇到一個跟上面的番外1以及0x03都有聯系的問題,也是我們App中一直存在的問題,就是進入Splash的時間過長的問題。為什么,之前錢包采用的是透明Activity的方案:
以前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)
;
說下后面的步驟:
獲取WindowManager
:ViewManager wm = a.getWindowManager()
WindowManager
會調用ViewRootImpl#setView
方法,并把DecorView
作為參數傳遞進去。
在這個方法內部,會通過跨進程的方式向WMS
(WindowManagerService)發起一個調用,從而將DecorView
最終添加到Window
上。
最后通過WMS
調用ViewRootImpl#performTraverals
方法開始View的測量、布局、繪制流程。
最終,ImageView
顯示出來。
所以,之前我在onCreate()
里,立刻去調用PermissionActivity,造成的問題跟上面的番外1一樣,都是在View
沒有加載完就做別的事情了。這里黑一下是因為,ImageView
還沒有繪制完成,但是SplashActivity是透明的,那段黑色的時間,是從Theme
到View
繪制完成的時間的間隔。
解決的辦法,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