Android View繪製流程

lostinai發表於2014-06-10

http://www.cnblogs.com/franksunny/archive/2012/04/20/2459738.html

框架分析

在之前的下拉重新整理中,小結過觸屏訊息先到WindowManagerService(Wms)然後順次傳遞給ViewRoot(派生自Handler),經decor view到Activity再傳遞給指定的View,這次整理View的繪製流程,通過原始碼可知,這個過程應該沒有涉及到IPC(或者我沒有發現),需要繪製時在UI執行緒中通過ViewRoot傳送一個非同步請求訊息,然後ViewRoot自己接收並不處理這個訊息。

在正式進入View繪製之前,首先需要明確一下Android UI的架構組成,偷圖如下:

 

上述架構很清晰的呈現了Activity、Window、DecorView(及其組成)、ViewRoot和WMS之間的關係,我通過原始碼簡單理了下從啟動Activity到建立View的過程,大致如下

 

在上圖中,performLaunchActivity函式是關鍵函式,除了新建被呼叫的Activity例項外,還負責確保Activity所在的應用程式啟動、讀取manifest中關於此activity設定的主題資訊以及上圖中對“6.onCreate”呼叫也是通過對mInstrumentation.callActivityOnCreate來實現的。圖中的“8. mContentParent.addView”其實就是架構圖中phoneWindow內DecorView裡面的ContentViews,該物件是一個ViewGroup類例項。在呼叫AddView之後,最終就會觸發ViewRoot中的scheduleTraversals這個非同步函式,從而進入ViewRoot的performTraversals函式,在performTraversals函式中就啟動了View的繪製流程。

performTraversals函式在2.3.5版本原始碼中就有近六百行的程式碼,跟我們繪製view相關的可以抽象成如下的簡單流程圖

 

流程圖中的host其實就是mView,而ViewRoot中的這個mView其實就是DecorView,之所以這麼說,又得具體看原始碼中ActivityThread的handleResumeActivity函式,在這裡我就不展開了。上述流程主要呼叫了View的measure、layout和draw三個函式。

measure過程分析

因為DecorView實際上是派生自FrameLayout的類,也即一個ViewGroup例項,該ViewGroup內部的ContentViews又是一個ViewGroup例項,依次內嵌View或ViewGroup形成一個View樹。所以measure函式的作用是為整個View樹計算實際的大小,設定每個View物件的佈局大小(“視窗”大小)。實際對應屬性就是View中的mMeasuredHeight(高)和mMeasureWidth(寬)。

在View類中measure過程主要涉及三個函式,函式原型分別為

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

前面兩個函式都是final型別的,不能過載,為此在ViewGroup派生的非抽象類中我們必須過載onMeasure函式,實現measure的原理是:假如View還有子View,則measure子View,直到所有的子View完成measure操作之後,再measure自己。ViewGroup中提供的measureChild或measureChildWithMargins就是實現這個功能的。

在具體介紹測量原理之前還是先了解些基礎知識,即measure函式的引數由類measureSpec的makeMeasureSpec函式方法生成的一個32位整數,該整數的高兩位表示模式(Mode),低30位則是具體的尺寸大小(specSize)。

MeasureSpec有三種模式分別是UNSPECIFIED, EXACTLY和AT_MOST,各表示的意義如下

如果是AT_MOST,specSize代表的是最大可獲得的尺寸;

如果是EXACTLY,specSize代表的是精確的尺寸;

如果是UNSPECIFIED,對於控制元件尺寸來說,沒有任何參考意義。

那麼對於一個View的上述Mode和specSize值預設是怎麼獲取的呢,他們是根據View的LayoutParams引數來獲取的:

引數為fill_parent/match_parent時,Mode為EXACTLY,specSize為剩餘的所有空間;

引數為具體的數值,比如畫素值(px或dp),Mode為EXACTLY,specSize為傳入的值;

引數為wrap_content,Mode為AT_MOST,specSize執行時決定。

具體測量原理

上面提供的Mode和specSize只是程式設計師對View的一個期望尺寸,最終一個View物件能從父檢視得到多大的允許尺寸則由子檢視期望尺寸和父檢視能力尺寸(可提供的尺寸)兩方面決定。關於期望尺寸的設定,可以通過在佈局資原始檔中定義的android:layout_width和android:layout_height來設定,也可以通過程式碼在addView函式呼叫時傳入的LayoutParams引數來設定。父View的能力尺寸歸根到最後就是DecorView尺寸,這個尺寸是全屏,由手機的解析度決定。期望尺寸、能力尺寸和最終允許尺寸的關係,我們可以通過閱讀measureChild或measureChildWithMargins都會呼叫的getChildMeasureSpec函式的原始碼來獲得,下面簡單列表說明下三者的關係

父檢視能力尺寸

子檢視期望尺寸

子檢視最終允許尺寸

EXACTLY + Size1

EXACTLY + Size2

EXACTLY + Size2

EXACTLY + Size1

fill_parent/match_parent

EXACTLY+Size1

EXACTLY + Size1

wrap_content

AT_MOST+Size1

AT_MOST+Size1

EXACTLY + Size2

EXACTLY+Size2

AT_MOST+Size1

fill_parent/match_parent

AT_MOST+Size1

AT_MOST+Size1

wrap_content

AT_MOST+Size1

UNSPECIFIED+Size1

EXACTLY + Size2

EXACTLY + Size2

UNSPECIFIED+Size1

fill_parent/match_parent

UNSPECIFIED+0

UNSPECIFIED+Size1

wrap_content

UNSPECIFIED+0

上述表格展現的是子檢視最終允許得到的尺寸,顯然1、4、7三項沒有對Size1和Size2進行比較,所以允許尺寸是可以大於父檢視的能力尺寸的,這個時候最終的檢視尺寸該是多少呢?AT_MOST和UNSPECIFIED的View又該如何決策最終的尺寸呢? 

通過Demo演示的得到的結果,假如Size2比Size1的尺寸大,假如不使用滾動效果的話,子檢視超出部分將被裁剪掉,該父檢視中如果在該子檢視後面還有其他檢視,那麼也將被裁剪掉,但是通過呼叫其getVisibility還是顯示該控制元件是可見的,所以裁剪後控制元件依然是有的,只是使用者沒辦法觀察到;在使用滾動效果的情況下,就能將原本被裁剪掉的控制元件通過滾動顯示出來。

對於第二個問題,根據原始碼View的OnMeasure函式呼叫的getDefaultSize函式獲知,預設情況下,控制元件都有一個最小尺寸,該值可以通過設定android:minHeight和android:minWidth來設定(無設定時預設為0);在設定了背景的情況下,背景drawable的最小尺寸與前面設定的最小尺寸比較,兩者取大者,作為控制元件的最小尺寸。在UNSPECIFIED情況下就選用這個最小尺寸,其它情況則根據允許尺寸來。不過這個是預設規則,通過demo發現,TextView在AT_MOST+Size情況下,並不是以Size作為控制元件的最終尺寸,結果發現在TextView的原始碼中,過載了onMeasure函式,有價值的程式碼如下:

……

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

……

if (widthMode == MeasureSpec.AT_MOST) {

    width = Math.min(widthSize, width);

}

……

if (heightMode == MeasureSpec.AT_MOST) {

    height = Math.min(desired, heightSize);

}

……

至於其中的width和desired值,感興趣的同學可以具體關注下。雖然FrameWork提供了檢視預設的尺寸計算規則,但是最終的檢視佈局大小可以過載onMeasure函式來修改計算規則,當然也可以不計算直接通過setMeasuredDimension來設定(需要注意的是,如果通過setMeasuredDimension的同時還要呼叫父類的onMeasure函式,那麼在呼叫父類函式之前呼叫的setMeasuredDimension會無效果)。

layout過程分析

上述measure過程達到的結果是設定了檢視的高和寬,layout過程的作用就是設定檢視在父檢視中的四個點(分別對應View四個成員變數mLeft,mTop,mLeft,mBottom)。同樣layout也是被fianl修飾符限定為不能過載,不過在ViewGroup中onLayout函式被abstract修飾,即所有派生自ViewGroup的類必須實現onLayout函式,從而實現對其包含的所有子檢視的佈局設定。

那麼上述的measure結果與layout有什麼關係,擷取ViewRoot和FrameLayout兩個類中onLayout函式的部分程式碼如下:

//ViewRoot的performTraversals函式measure之後對layout的呼叫程式碼

host.layout(0, 0, host.mMeasuredWidthhost.mMeasuredHeight);

//FrameLayou的onLayout函式部分原始碼

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        final int count = getChildCount();

        ……

        for (int i = 0; i < count; i++) {

            final View child = getChildAt(i);

            if (child.getVisibility() != GONE) {

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();

                final int height = child.getMeasuredHeight();

                int childLeft = parentLeft;

                int childTop = parentTop;

                final int gravity = lp.gravity;

 

                if (gravity != -1) {

                    final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

                    final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

 

                    switch (horizontalGravity) {

                        case Gravity.LEFT:

                            childLeft = parentLeft + lp.leftMargin;

                            break;

                        case Gravity.CENTER_HORIZONTAL:

                            childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;

                            break;

                        case Gravity.RIGHT:

                            childLeft = parentRight - width - lp.rightMargin;

                            break;

                        default:

                            childLeft = parentLeft + lp.leftMargin;

                    }

 

                    switch (verticalGravity) {

                        case Gravity.TOP:

                            childTop = parentTop + lp.topMargin;

                            break;

                        case Gravity.CENTER_VERTICAL:

                            childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;

                            break;

                        case Gravity.BOTTOM:

                            childTop = parentBottom - height - lp.bottomMargin;

                            break;

                        default:

                            childTop = parentTop + lp.topMargin;

                    }

                }

 

                child.layout(childLeft, childTop, childLeft + width, childTop + height);

            }

        }

    }

從程式碼顯然可知具體layout佈局時,就是根據measure過程設定的高和寬,結合檢視在父檢視中的起始位置,再外加檢視的layoutgravity屬性來設定四個點的具體位置(在LinearLayout中還會增加對layoutweight屬性的考慮)。這個過程相對沒有measure那麼複雜。

需要注意的是在自定義組合控制元件的時候,我們可以根據需要不用或只用部分measure過程計算得到的尺寸,具體可以看下之前做的下拉重新整理控制元件直接過載的onLayout函式:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

    if (getChildCount() > 2) {

        throw new IllegalStateException("NPullToFreshContainer can host only two direct child");

    }

        

    View headView = getChildAt(0);

    View contentView = getChildAt(1);

    if(headView != null){

     headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值

    }

   

    if(contentView != null){

    contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());

    }

        

    if (mFirstLayout) {        

     HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();

       mFirstLayout = false;

    }

}

draw過程分析

View的Draw過程,其實相對來說應該比measure過程更為複雜,正因為其很複雜,所以android框架層已經將draw過程考慮得相當周全,雖然view類的Draw函式沒用final修飾,但是我們自定義的View,一般也不需要去過載實現它,自己目前也沒有自己去draw過介面,對整個過程,只能偷別人整理的邏輯,結合原始碼瀏覽了一下,在這裡做個標註。

draw()方法實現的功能流程如下:

1、呼叫background.draw(canvas)繪製該View的背景

2、呼叫onDraw(canvas)方法繪製檢視本身(每個View都需要過載該方法,ViewGroup不需要實現該方法)

3、呼叫dispatchDraw(canvas)方法繪製子檢視(ViewGroup類已經為我們重寫了dispatchDraw ()的功能實現,其內部會遍歷每個子檢視,呼叫drawChild()去重新回撥每個子檢視的draw()方法)

4、呼叫onDrawScrollBars(canvas)繪製滾動條

為了說明measure、layout和draw過程的連續性,摘得draw中的原始碼如下

……

if (mBackgroundSizeChanged) {

    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);

    mBackgroundSizeChanged = false;

}

……

上述的mLeft,mTop,mLeft,mBottom就是我們在layout是設定的結果值,這裡之所以要用減法獲取高寬尺寸而不用measure過程設定的mMeasuredHeight和mMeasureWidth,個人感覺就是因為我們可以在程式碼中通過直接呼叫View的layout函式避開measure測算結果而導致真實高寬不等於mMeasuredHeight和mMeasureWidth這種情況。

上述程式碼中的mBackgroundSizeChanged是個私有成員變數,原始碼中只能在View的onScrollChanged(int l, int t, int oldl, int oldt) 、layout過程呼叫的setFrame(int left, int top, int right, int bottom) 和setBackgroundDrawable(Drawable d)這三個函式中對其修改為true。

到這裡,除了具體的繪製外,我們對從Activity到View的繪製流程應該比較清楚了。

 

 

 

本文除了參閱原始碼,發現下面兩篇博文幫助很大,有興趣可以詳細閱讀

http://blog.csdn.net/qinjuning/article/details/7110211

http://www.cnblogs.com/bastard/archive/2012/04/10/2440577.html

驗證View measure現象的demo見 http://files.cnblogs.com/franksunny/ViewDemo.rar

由於文件中的圖片沒有顯示出來,所以上傳一個pdf文件,方便查閱 http://files.cnblogs.com/franksunny/AndroidView%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B.pdf


相關文章