View的三次measure,兩次layout和一次draw


我在《Android視圖結構》這篇文章中已經描述了Activity,WindowView在視圖架構方面的關系。前天,我突然想到為什么在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,onLayoutonDraw函數呢?

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嗎?那你就太天真了!我們都知道,視圖結構中不僅僅是DecorViewFrameLayout,還有其他的需要兩次measure子視圖的ViewGroup,如果每次都導致子視圖兩次measure,那效率就太低了。所以Viewmeasure函數中有相關的機制來防止這種情況。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ......
  // 當FLAG_FORCE_LAYOUT位為1時,就是當前視圖請求一次布局操作
  //或者當前當前widthSpec和heightSpec不等於上次調用時傳入的參數的時候
  //才進行從新繪制。
    if (forceLayout || !matchingSize &&
            (widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec)) {
            ......
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            ......
    }
    ......
}  

源碼看到這里,我幾乎失望的眼淚掉下來!沒辦法,只能再想其他的方法來分析這個問題。

斷點調試大法好

 為了分析函數調用的層級關系,我想到了斷點調試法。於是,我果斷在onMeasureonLayout函數中設置了斷點,然后進行調試。


函數調用幀

 在《Android視圖架構》一文中,我們知道ViewRootDecorView的父視圖,雖然它自己並不是一個View,但是它實現了ViewParent的接口,Android正是通過它來實現整個視圖系統的初始化工作。而它的performTraversals函數就是視圖初始化的關鍵函數。 
 對比兩次onMeasure被調用時的函數調用幀,我們可以輕易發現ViewRootImplperformTraversals函數中直接和間接的調用了兩次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為真,而第二次時是假。 
斷點調試截圖

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM