我在《Android視圖結構》這篇文章中已經描述了Activity
,Window
和View
在視圖架構方面的關系。前天,我突然想到為什么在setContentView
中能夠調用findViewById
函數?View
那時不是還沒有被加載,測量,布局和繪制啊。然后就搜索了相關的條目,發現findViewById
只需要在inflate
結束之后就可以。於是,我整理了Activity生命周期和View的生命周期的關系,並再次做一下總結。
為了節約你的時間,本篇文章的主要內容為:
- Activity的生命周期和它包含的View的生命周期的關系
- Activity初始化時View為什么會三次measure,兩次layout但只一次draw?
- ViewRoot的初始化過程
Activity的生命周期和View的生命周期
我通過一個簡單的demo來驗證Activity
生命周期方法和View
的生命周期方法的調用先后順序。請看如下截圖
在onCreate
函數中,我們通常都調用setContentView
來設置布局文件,此時Android系統就會讀取布局文件,但是視圖此時並沒有加載到Window
上,並且也沒有進入自己的生命周期。
只有等到Activity
進入resume狀態時,它所擁有的View
才會加載到Window
上,並進行測量,布局和繪制。所以我們會發現相關函數的調用順序是:
- onResume(Activity)
- onPostResume(Activity)
- onAttachedToWindow(View)
- onMeasure(View)
- onMeasure(View)
- onLayout(View)
- onSizeChanged(View)
- onMeasure(View)
- onLayout(View)
-
onDraw(View)
大家會發現,為什么
onMeasure
先調用了兩次,然后再調用onLayout
函數,最后還有在依次調用onMeasure
,onLayout
和onDraw
函數呢?
ViewGroup的measure
大家應該都知道,有些ViewGroup
可能會讓自己的子視圖測量兩次。比如說,父視圖先讓每個子視圖自己測量,使用View.MeasureSpec.UNSPECIFIED
,然后在根據每個子視圖希望得到的大小不超過父視圖的一些限制,就讓子視圖得到自己希望的大小,否則就用其他尺寸來重新測量子視圖。這一類的視圖有FrameLayout
,RelativeLayout
等。
在《Android視圖結構》中,我們已經知道Android視圖樹的根節點是DecorView
,而它是FrameLayout
的子類,所以就會讓其子視圖繪制兩次,所以onMeasure
函數會先被調用兩次。
// FrameLayout的onMeasure函數,DecorView的onMeasure會調用這個函數。 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); ..... for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); ...... } } ........ count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { ........ child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
你以為到了這里就能解釋通View初始化時的三次measure,兩次layout卻只一次draw嗎?那你就太天真了!我們都知道,視圖結構中不僅僅是DecorView
是FrameLayout
,還有其他的需要兩次measure子視圖的ViewGroup
,如果每次都導致子視圖兩次measure,那效率就太低了。所以View
的measure
函數中有相關的機制來防止這種情況。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ...... // 當FLAG_FORCE_LAYOUT位為1時,就是當前視圖請求一次布局操作 //或者當前當前widthSpec和heightSpec不等於上次調用時傳入的參數的時候 //才進行從新繪制。 if (forceLayout || !matchingSize && (widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec)) { ...... onMeasure(widthMeasureSpec, heightMeasureSpec); ...... } ...... }
源碼看到這里,我幾乎失望的眼淚掉下來!沒辦法,只能再想其他的方法來分析這個問題。
斷點調試大法好
為了分析函數調用的層級關系,我想到了斷點調試法。於是,我果斷在onMeasure
和onLayout
函數中設置了斷點,然后進行調試。
在《Android視圖架構》一文中,我們知道ViewRoot
是DecorView
的父視圖,雖然它自己並不是一個View
,但是它實現了ViewParent
的接口,Android正是通過它來實現整個視圖系統的初始化工作。而它的performTraversals
函數就是視圖初始化的關鍵函數。
對比兩次onMeasure
被調用時的函數調用幀,我們可以輕易發現ViewRootImpl
的performTraversals
函數中直接和間接的調用了兩次performMeasure
函數,從而導致了View
最開始的兩次measure過程。
然后在相同的performTraversals
函數中會調用performLayout
函數,從而導致View
進行一輪layout過程。
但是為什么這次performTraversals
並沒有觸發View
的draw過程呢?反而是View
又將重新進行一輪measure,layout過程之后才進行draw。
兩次performTraversals
通過斷點調試,我們發現在View
初始化的過程中,系統調用了兩次performTraversals
函數,第一次performTraversals
函數導致了View的前兩次的onMeasure
函數調用和第一次的onLayout
函數調用。后一次的performTraversals
函數導致了最后的onMeasure
,onLayout
,和onDraw
函數的調用。但是,第二次performTraversals
為什么會被觸發呢?我們研究一下其源碼就可知道。
private void performTraversals() { ...... boolean newSurface = false; //TODO:決定是否讓newSurface為true,導致后邊是否讓performDraw無法被調用,而是重新scheduleTraversals if (!hadSurface) { if (mSurface.isValid()) { // If we are creating a new surface, then we need to // completely redraw it. Also, when we get to the // point of drawing it we will hold off and schedule // a new traversal instead. This is so we can tell the // window manager about all of the windows being displayed // before actually drawing them, so it can display then // all at once. newSurface = true; ..... } } ...... if (!cancelDraw && !newSurface) { if (!skipDraw || mReportNextDraw) { ...... performDraw(); } } else { if (viewVisibility == View.VISIBLE) { // Try again scheduleTraversals(); } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).endChangingAnimations(); } mPendingTransitions.clear(); } } ...... }
由源代碼可以看出,當newSurface
為真時,performTraversals
函數並不會調用performDraw
函數,而是調用scheduleTraversals
函數,從而再次調用一次performTraversals
函數,從而再次進行一次測量,布局和繪制過程。
我們由斷點調試可以輕易看到,第一次performTraversals
時的newSurface
為真,而第二次時是假。