天天看點

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

 我在 《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)
  • onLayout(View)
  • onSizeChanged(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

為真,而第二次時是假。

斷點調試截圖

總結

 雖然我已經通過源碼得知View初始化時measure三次,layout兩次,draw一次的原因,但是Android系統設計時,為什麼要将整個初始化過程設計成這樣?我卻還沒有明白,為什麼當

Surface

為新的時候,要推遲繪制,重新進行一輪初始化,這些可能都要涉及到

Surface

的相關内容,我之後要繼續學習相關内容!

繼續閱讀