View的工做原理

前言

在Android中View一直扮演着一個很重要的角色,它是咱們開發中視覺的呈現,我日常也使用着Android提供的豐富且功能強大的控件,有時候遇到一個很炫酷的自定義View的開源庫,咱們也是拿來主義,時間長了你就會發現你只是一個只會使用控件和依賴被人開源庫的程序員,這並非一個開發者,因此咱們並不能只知足於使用,咱們要理解它背後的工做原理和流程,這樣才能本身作出一個屬於本身的控件,一直都說自定View是Android進階中的一道門檻,當其實自定義View當你理解了它的原理後,你就會發現它也不過如此。本文將從源碼的角度探討View工做的三大流程,對View作進一步的認識。俗話說的好:源碼纔是最好的老師。html

本文代碼基於Android8.0,相關源碼位置以下:
frameworks/base/core/java/android/*.java(*表明View, ViewGroup, ViewRootImpl)
frameworks/base/core/java/android/FrameLayout.java
複製代碼

View什麼時候開始繪製?- requestLayout()

提到View,就不得不講起Window,在Window,WindowManager和WindowManagerService之間的關係文章中講過,Widnow是View得載體,在ViewRootImpl的setView方法中添加Winodw到WMS以前,會先調用requestLayout繪製整顆View Hierarchy的繪製,以下:java

因此咱們先從requestLayout()中看起,該方法以下:android

//ViewRootImpl.java
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //檢查是否在主線程,在子線程繪製UI會拋出異常,見下方
        checkThread();
        //是否measure和layout佈局的開關
        mLayoutRequested = true;
        //一、準備開始遍歷View Hierarchy繪製
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
            "Only the original thread that created a view hierarchy can touch its views.");
    }
}
複製代碼

requestLayout()中首先會檢查線程的合法性,Android規定必須在主線程中操做UI,那麼爲何不能在子線程中訪問UI呢?這是由於Android的UI控件都不是線程安全的,若是在多線程環境下併發訪問控件會致使控件處於不可預測狀態。接着咱們來看註釋1,調用了ViewRootImpl的scheduleTraversals方法,以下:程序員

//ViewRootImpl.java
void scheduleTraversals() {
        if (!mTraversalScheduled) {//防止同一幀繪製屢次
            mTraversalScheduled = true;
            //攔截同步Message,優先處理異步Message
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
             //一、Choreographer回調,裏面執行最終會執行mTraversalRunnable中的繪製任務
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            //...
        }
    }

複製代碼

在Android4.1以前Android的UI流暢性不好,因此在Android4.1以後引入了Choreographer機制和Vsync機制用來解決這個問題,Choreographer管理者動畫、輸入和繪製的時機,Vsync叫Vertical Synchronization(垂直同步)信號,每隔 16ms Choreographer就會收到來自native層的Vsync信號,這時Choreographer就會根據事件類型進行相應的回調操做,Choreographer支持4種事件類型回調:輸入(CALLBACK_INPUT)、繪製(CALLBACK_TRAVERSAL)、動畫(CALLBACK_ANIMATION)、提交(CALLBACK_COMMIT),並經過postCallback方法在對應須要同步Vsync刷新處進行註冊,等待回調,關於這個細節和原理能夠看Android圖形系統-ChoreographerAndroid垂直同步和三重緩存,這裏咱們並不深究Choreographer機制和Vsync機制,咱們看到註釋1中的Choreographer的postCallback方法提交了CALLBACK_TRAVERSAL類型的回調,它對應着mTraversalRunnable繪製操做,而mTraversalRunnable是一個TraversalRunnable類型的繪製任務,最終回調會執行這個任務,mTraversalRunnable的run方法源碼以下:canvas

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //一、裏面會執行performTraversals()
        doTraversal();
    }
}
複製代碼

doTraversal()裏面會執行performTraversals方法,點開doTraversal方法看一下,以下:緩存

//ViewRootImpl.java
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除攔截同步Message屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        //一、今天的主角,performTraversals()方法
        performTraversals();
        //...
    }
}
複製代碼

在doTraversal() 方法裏面咱們終於看到咱們熟悉的方法:performTraversals()安全

View樹繪製的起點 - performTraversals()

performTraversals()它是整個View Hierarchy繪製的起點,它裏面會執行View繪製的三大工做流程,咱們先看一下精簡版的performTraversals方法,以下:多線程

//ViewRootImpl.java
private void performTraversals() {
    //mView是在View與ViewRootImpl創建關聯的時候被賦值的,即調用ViewRootImpl的setView方法時,它表明着View Hierarchy的根節點,即根視圖
    final View host = mView;
    //...
    WindowManager.LayoutParams lp = mWindowAttributes;
    //desiredWindowWidth和desiredWindowHeight分別表明着屏幕的寬度和高度
    int desiredWindowWidth;
    int desiredWindowHeight;
    //...
    if (mLayoutRequested) {
        final Resources res = mView.getContext().getResources();
        //...
        //一、這裏調用了measureHierarchy方法,裏面會調用performMeasure方法,執行View Hierarchy的measure流程,見下方
        windowSizeMayChange |= measureHierarchy(host, lp, res,
     	                                         desiredWindowWidth, desiredWindowHeight);
        //...
    }
    //...
    if(didLayout){
          //二、這裏調用了performLayout方法,執行View Hierarchy的layout流程
         performLayout(lp, mWidth, mHeight);
        //...
    }
    //...
    if (!cancelDraw && !newSurface) {
        //...
        //三、這裏調用了performDraw方法,執行View Hierarchy的draw流程
   		performDraw();
    }
    //...
}

 private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
     int childWidthMeasureSpec;
     int childHeightMeasureSpec;
     //...
     //1.一、頂級View在調用performMeasure方法以前,會先調用getRootMeasureSpec方法來生成自身寬和高的MeasureSpec
     childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
     childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
     //1.二、這裏調用performMeasure方法,執行View Hierarchy的measure流程
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 }
複製代碼

performTraversals方法裏面很是複雜,咱們看的時候千萬不要深究其中的細節,否則就走火入魔了,咱們找出個總體框架就行,咱們先看註釋一、二、3,能夠看到依此調用**measureHierarchy() -> performLayout() -> performDraw(),而measureHierarchy()裏面最終調用performMeasure(),因此performTraversals()能夠看做依此調用了performMeasure() -> performLayout() -> performDraw(),分別對應頂級View的measure、layout和draw流程,**頂級View能夠理解爲View Hierarchy的根節點,它通常是一個ViewGroup,就像Activity的DecorView同樣。併發

ps:app

一、在performTraversals()方法中,performMeasure()可能會執行屢次,而performLayout()和performDraw()最多執行一次。

二、本文討論的頂級View你能夠把它類比成Activity的DecorView,可是它其實就是View樹的根結點,DecorView也是Activity中View樹的根結點。

接下來咱們就照着performTraversals() 中的總體框架來說解View工做的三大流程。

View的測量流程 - performMeasure()

一、MeasureSpec

講解View的measure流程前,不得不先講解一下MeasureSpec的含義,MeasureSpec是一個32位的int值,它是View的一個內部類,它的高2位表明着SpecMode,表示測量模式,它的低30位表示SpecSize,表示測量大小,系統經過位運算把SpecMode和SpecSize合二爲一組成一個32位int值的MeasureSpec。

下面看一下MeasureSpec的裏面組成,以下:

//View.java
public static class MeasureSpec {
    //左移位數
    private static final int MODE_SHIFT = 30;
    //位掩碼
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //表明着三種SpecMode
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    //makeMeasureSpec方法是把SpecMode和SpecSize經過位運算組成一個MeasureSpec並返回
    public static int makeMeasureSpec(int size,int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    //getMode方法是從給定的MeasureSpec中取出SpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    //getSize方法是從給定的MeasureSpec中取出SpecSize
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}
複製代碼

能夠看到MeasureSpec提供了三個工具方法分別用來組合MeasureSpec、從MeasureSpec中取出SpecMode、從MeasureSpec中取出SpecSize,其中SpecMode有三種取值,以下:

  • UNSPECIFIED:它表示父容器對子View的繪製的大小沒有任何限制,要多大給多大,這種狀況通常適用於系統內部,表示一種測量狀態。
  • EXACTLY:它表示父容器已經測量出子View須要的精確大小SpecSize,這個時候View的最終大小就是SpecSize的值,它對應於LayoutParams中match_parcent和具體的數值這兩種模式。
  • AT_MOST:它表示父容器爲子View的大小指定了一個最大值SpecSize,這個時候View的大小不能大於這個值,它對應於LayoutParams中的wrap_content這種模式。

1.1 如何肯定View的MeasureSpec?

除了頂級View,其餘View的MeasureSpec都是由父容器的MeasureSpec和自身的LayoutParams共同決定的,LayoutParams就是你平時在編寫View的xml屬性時那些帶有layout_XX前綴開頭的佈局屬性,對於頂級View和在View樹中子View的MeasureSpec的生成規則有點不同,見下面分析:

1.1.一、頂級View的MeasureSpec的建立 - getRootMeasureSpec()

因爲頂級View是View樹的根結點,因此它沒有父容器,因此它的MeasureSpec是由屏幕窗口的尺寸和自身的LayoutParams來共同決定,上面註釋1.1咱們講到頂級View在調用performMeasure方法以前,會先調用ViewRootImpl的getRootMeasureSpec方法來生成自身寬和高的MeasureSpec,咱們來看一下getRootMeasureSpec方法,以下:

//ViewRootImpl.java
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT://若是是MATCH_PARENT,那麼就是EXACTLY
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT://若是是WRAP_CONTENT,就是AT_MOST
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default://若是是固定的值,也是EXACTLY
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}
複製代碼

windowSize就是是傳入的desiredWindowWidth或desiredWindowHeight,它表示屏幕的大小,rootDimension就是傳入的屏幕窗口的LayoutParams的大小模式,對應咱們平時寫的layout_width或layout_height屬性,該屬性無非就三個值:match_parent、wrap_content和固定的數值,因此從getRootMeasureSpec方法能夠看到,頂級View的MeasureSpec的建立規則以下:

其中rootSize表示頂級View大小。

1.1.二、子View的MeasureSpec的建立 - getChildMeasureSpec()

在1中,頂級View的MeasureSpec已經建立好了,這時候就要根據這個MeasureSpec去生成子View的MeasureSpec,子View的MeasureSpec的建立是從ViewGroup的measureChildWithMargins方法開始,以下:

//ViewGroup.java
rotected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
    //獲得子View的margin
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //一、這裏調用了getChildMeasureSpec方法,裏面就是建立子View的MeasureSpec,這裏建立子View寬的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
    //同理,這裏建立子View高的MeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height);
    //若是子View是一個ViewGroup,遞歸measure下去
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

複製代碼

上述方法會對子View進行measure,由註釋1得知,在調用子View的measure方法前,會先調用getChildMeasureSpec方法得到子View的MeasureSpec,從getChildMeasureSpec方法的參數能夠看出,子View的MeasureSpec的建立與父容器的MeasureSpec和子View自己的LayoutParams有關,此外還和View的margin及padding有關,下面咱們來看ViewGroup的getChildMeasureSpec方法,以下:

//ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //取出父容器的測量模式specMode
    int specMode = MeasureSpec.getMode(spec);
    //取出父容器的測量大小specSize
    int specSize = MeasureSpec.getSize(spec);
	// padding是指父容器中已佔用的空間大小,所以子View最大可用大小size == 父容器剩餘大小 == 父容器的尺寸減去padding 
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
        case MeasureSpec.EXACTLY://若是父容器是EXACTLY
            if (childDimension >= 0) {//若是子View的LayoutParams是固定大小
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//若是子View的LayoutParams是MATCH_PARENT //子View的MeasureSpec爲父容器剩餘大小 + EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//若是子View的LayoutParams是WRAP_CONTENT
                //子View的MeasureSpec爲父容器剩餘大小 + AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST://若是父容器是AT_MOST
            if (childDimension >= 0) {
                //子View的MeasureSpec爲子View大小 + EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子View的MeasureSpec爲父容器剩餘大小 + AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
               //子View的MeasureSpec爲父容器剩餘大小 + AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.UNSPECIFIED://若是父容器是UNSPECIFIED,這個平時開發用不到
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼

能夠看到getChildMeasureSpec方法裏面的邏輯仍是很清楚的,首先根據父容器的測量模式specMode分爲三大類:**EXACTLY、AT_MOST和UNSPECIFIED,**每一類又和子View的LayoutParams的的三種大小模式:固定大小、MATCH_PARENT和WRAP_CONTENT組合,因此總共有3 X 3 = 9種組合,因此根據getChildMeasureSpec方法能夠得出子View的MeasureSpec的建立規則以下:

其中childSize表示子View的大小,parentSize表示父容器剩餘大小。

二、View和ViewGroup的measure流程

分析完View的MeasureSpec的建立後,咱們繼續回到View的measure流程,你們都知道ViewGroup是繼承自View的,因此View的measure流程,分爲兩種狀況,一種是View的measure流程,一種是ViewGroup的measure流程,可是無論是View的measure流程仍是ViewGroup的measure流程都是從ViewRootImpl的performMeasure()開始,而且都會先調用View的measure方法,以下:

//ViewRootImpl.java 
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    //...
    //一、調用了View的measure方法
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼

咱們繼續看View的measure方法,以下:

//View.java
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      //...
      //一、調用了onMeasure方法
      onMeasure(widthMeasureSpec, heightMeasureSpec);
	 //...
  }
複製代碼

能夠看到measure方法是一個final方法,說明這個方法不可以被子類重寫,這個方法把measure的具體過程交給了onMeasure方法去實現,因此View和ViewGroup的measure流程的差別就從這個onMeasure方法開始,見下面分析。

2.一、View的measure流程

從上述知道View的measure起點在View的measure方法中,而且View的measure方法會調用View的onMeasure方法,View::measure() -> View::onMeasure(),因此咱們直接看onMeasure方法在View中的實現,以下:

//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //一、 若是View沒有重寫onMeasure方法,則會調用setMeasuredDimension方法設置寬高,在設置以前先調用getDefaultSize方法獲取默認寬高
    setMeasuredDimension(
        			    getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}
複製代碼

View中的onMeasure方法的默認實現是先調用getDefaultSize方法獲取默認寬高,而後再調用調用setMeasuredDimension方法設置View的寬高,當調用setMeasuredDimension方法設置View的寬高後,就能夠經過getMeasureWidth()或getMeasureHeight()得到View測量的寬高,因此咱們先看一下 getDefaultSize()方法是如何獲取默認的寬高,該方法源碼以下:

//View.java
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        //若是specMode是UNSPECIFIED,返回的大小就是傳進來的size,而這個size就是經過getSuggestedMinimumWidth()或getSuggestedMinimumHeight()方法得到的
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        //若是specMode是AT_MOST或EXACTLY,返回的大小就是MeasureSpec中的specSize
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

複製代碼

getDefaultSize方法的邏輯很簡單,除了UNSPECIFIED這種模式,其餘測量模式都返回MeasureSpec中的specSize,而這個specSize就等於父容器給View測量後的大小,因此咱們能夠得出一個結論:直接繼承View寫自定義控件時須要重寫onMeasure方法並設置wrap_content時自定義View自身的大小,這是由於若是自定義View在xml文件寫了layout_XX = wrap_content這個屬性,那麼在建立它的MeasureSpec時,它的specMode就會等於AT_MOST,而從getDefaultSize方法看出,若是specMode是AT_MOST或EXACTLY,它們兩個返回的值是同樣的,都是MeasureSpec中的specSize,經過上面所講的子View的MeasureSpec的建立規則可知specSize是等於parentSize即父容器剩餘的大小,這樣就會形成這個自定義View會填充滿整個父容器,效果和match_parent同樣,並不按你想象那樣的大小。因此之後在自定義View時,若是有wrap_content這個場景,就要重寫onMeasure方法,能夠參考下面的模板,以下:

//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        			      	 
     int measureWidth = MeasureSpec.getSize(widthMeasureSpec); 
     int measureHeight = MeasureSpec.getSize(heightMeasureSpec);  
     int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);   
     int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);  
     int width, height;
     //通過計算,控件所佔的寬和高分別對應width和height
     // ………… 
     //咱們只須要在View爲wrap_content時設置咱們通過計算得出的View的默認寬高width和height便可
     //其餘模式如EXACTLY,就直接設置父容器給咱們測量出來的寬高便可
 	 setMeasuredDimension(
         (measureWidthMode == MeasureSpec.AT_MOST) ? width : measureWidth , 
         (measureHeightMode == MeasureSpec.AT_MOST) ? height : measureHeight
     );
}

複製代碼

講完了getDefaultSize()中AT_MOST和EXACTLY模式狀況,接着講UNSPECIFIED這種模式的狀況,從getDefaultSize方法中能夠看出若是specMode是UNSPECIFIED,返回的大小就是傳進來的size,而這個size就是經過getSuggestedMinimumWidth()或getSuggestedMinimumHeight()方法得到的,因此咱們以getSuggestedMinimumWidth方法爲例子,看一些若是獲取在UNSPECIFIED模式下的寬,getSuggestedMinimumHeight()方法同理,getSuggestedMinimumWidth方法源碼以下:

//View.java 
protected int getSuggestedMinimumWidth() {
    //根據View有無背景返回大小,getMinimumWidth()見下方
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

//Drawable.java
public int getMinimumWidth() {
    //getIntrinsicWidth()返回Drawable的寬,默認返回-1
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
複製代碼

mBackground就等於View的背景,即android:background屬性,mMinWidth就等於你在View的xml佈局中寫了「android:minWidth」這個屬性,mBackground.getMinimumWidth()就是獲取View的背景的寬度,因此咱們得出結論:在UNSPECIFIED模式下,若是View沒有設置背景,那麼View的寬就等於android:minWidth,若是View設置了背景,那麼View的寬就等於View的背景background的寬和android:minWidth的最大值,高度同理

View的onMeasure方法執行完後,就能夠經過getMeasureWidth()或getMeasureHeight()得到View測量的寬高,可是有可能會不許確,由於有時候系統會進行屢次measure,才能肯定最終測量寬高,因此最好是在onLayout方法中去獲取View的寬高。

2.二、ViewGroup的measure流程 (以FrameLayout爲例)

從上述知道ViewGroup的measure起點也在View的measure方法中,而View的measure方法會調用View的onMeasure方法,ViewGroup繼承自View,可是它是一個抽象類並無重寫View的onMeasure方法,而是由ViewGroup的子類如LinearLayout、FrameLayout等重寫onMeasure方法以實現不一樣的measure流程,這裏以FrameLayout爲例,**View::measure() -> FrameLayout::onMeasure() **,咱們來看FrameLayout的onMeasure方法,以下:

//FrameLayout.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //獲取子View的個數
    int count = getChildCount();
    //...
    //遍歷全部子View,測量每一個子View的大小
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {//若是子View可見
            //一、調用ViewGroup的measureChildWithMargins方法,測量子View的大小
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //子View測量完後,FrameLayout就能夠經過View的getMeasuredWidth或getMeasuredHeight得到子View的寬高,從而得出本身的寬高
            //根據FrameLayout的疊加特性,它自身的測量寬高就是全部子View寬高中的最大值
            maxWidth = Math.max(maxWidth,
                                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                                 child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        }
    }
    //...
}
複製代碼

能夠看到與View的onMeasure方法不一樣的是,FrameLayout的onMeasure方法是遍歷它全部的子View,而後逐個測量子View的大小,這個測量子View是經過註釋1的measureChildWithMargins方法來完成,這個方法已經在上面子View的MeasureSpec的建立中講過一點,measureChildWithMargins方法是在FrameLayout的父類ViewGroup中,以下:

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
    //省略的這部分在上面已經講過,主要是建立子View的MeasureSpec(childWidthMeasureSpec, childHeightMeasureSpec)
    //...
    //一、調用子View的measure方法,叫子View本身測量本身
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼

measureChildWithMargins方法中首先會根據父容器傳進來的parenXXMeasureSpec來建立子View的childXXMeasureSpec,而後調用子View的measure方法,把測量子View的任務又推給了子View,這個過程又回到了2.1所講的View的measure流程,就再也不贅述,全部子View測量完後,ViewGroup就能夠得出本身的測量寬高。

三、小結

measure流程是三大流程中最複雜的一個,它的總體流程是:從ViewRootImp的performTraversals()方法進入performMeasure()方法,開始整顆View樹的測量流程,在performMeasure方法裏面會調用View的measure方法,而後measure方法會調用onMeasure方法,若是是View就直接開始測量,設置View的寬高,若是是ViewGroup,則在onMeasure方法中則會對全部的子View進行measure過程,若是子View是一個ViewGroup,那麼繼續向下傳遞,直到全部的View都已測量完成。如圖:

measure事後就能夠經過getMeasureWidth()或getMeasureHeight()得到View測量的寬高。

View的佈局流程 - performLayout()

前面講解了View的measure過程,若是你理解了,那麼View的佈局過程也很容易理解的,和measure類似,View的佈局過程是從ViewRootImpl的performLayout()開始的,以下:

//ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    //...
    final View host = mView;
    //...
    //一、調用了頂級View的layout方法
     host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    //...
}
複製代碼

在performLayout中主要調用了頂級View的layout方法,頂級View的實例有多是View也有多是ViewGroup,可是這個layout方法是在View中,它不像measure方法那樣,它不是final修飾,因此它能夠被重寫,而且ViewGroup重寫了layout方法,咱們先看一下ViewGroup中的layout方法,以下:

//ViewGroup.java
@Override
public final void layout(int l, int t, int r, int b) {
    if (...) {
        //...
         //一、ViewGroup中的重寫的layout方法仍是調用了父類即View的layout方法
        super.layout(l, t, r, b);
    } else {
       //...
    }
}
複製代碼

能夠看到ViewGroup重寫的layout方法只是作了一些判斷,而後最終仍是仍是調用了父類即View的layout方法,因此咱們直接看View的layout方法便可。

一、View和ViewGroup的layout流程

View的layout方法以下:

//View.java
public void layout(int l, int t, int r, int b) {
    // 注意傳進來的四個參數:
    // l 表示子View的左邊緣相對於父容器的上邊緣的距離
    // t 表示子View的上邊緣相對於父容器的上邊緣的距離
    // r 表示子View的右邊緣相對於父容器的右邊緣的距離
    // b 表示子View的下邊緣相對於父容器的下邊緣的距離
    //...
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    //一、調用setFrame方法設定View的四個頂點的位置
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //二、調用onlayout方法
	    onLayout(changed, l, t, r, b);
        //...
    }
	//...
}
複製代碼

layout方法傳進來的l、t、r、b分別表明着View的上下左右四個點的座標,這個四個點的座標是相對於它的父容器來講的,這個layout方法主要乾了兩件事:

  • 一、註釋1:調用View的setFrame方法設定View的四個頂點的位置,咱們先看View的setFrame()方法,以下:
//View.java
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        //...
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        //...
    }
    return changed;
}
複製代碼

能夠看到,setFrame方法主要把l、t、r、b分別賦值給mLeft、mTop、mBottom、mRight,即更新View的四個頂點的位置,這個四個頂點一旦肯定,那麼View在父容器中的位置也就肯定了。

  • 二、咱們繼續看註釋2:調用了onLayout方法,這個方法在View中是一個空實現,以下:
//View.java 
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
複製代碼

可是在ViewGroup中是一個抽象方法,以下:

//ViewGroup.java
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
複製代碼

這是由於onLayout方法主要用途是給父容器肯定子View的位置,因此若是自己就是一個View,就無需實現這個方法,可是若是是ViewGroup,它還要佈局子View,因此是ViewGroup的子類就要強制實現這個方法,不一樣的ViewGroup具備不一樣的佈局方式,因此不一樣的ViewGroup的onLayout方法的實現就不同,咱們仍是以FrameLayout爲例,看一下FrameLayout的onLayout方法的實現,以下:

//FrameLayout.java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

複製代碼

FrameLayout的onLayout方法只調用了layoutChildren方法,該方法以下:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
		//獲取padding值
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
		//遍歷全部子View,佈局每一個子View
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //若是子View可見
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //得到measue流程測量出來的子View的寬高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                //子View的左邊緣位置
                int childLeft;
                //子View的上邊緣位置
                int childTop;
                //下面是獲取佈局方向
                //...
                //下面根據佈局方向計算出childLeft和childTop
                //...
			   //一、根據上面的計算,就算出了一個子View的左邊緣位置childLeft和上邊緣位置childTop
                //從而根據childLeft和childTop得出子View的右邊緣位置childRight = childLeft + width,下邊緣位置childButtom = childTop + height
                //而後調用子View的layout方法
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
複製代碼

能夠發現layoutChildren裏面過程和onMeasure裏面的過程很像,只是註釋1中調用的是子View的layout方法而不是measure方法,若是這個子View是一個View,那麼layout方法裏面就能夠經過setFrame方法直接肯定自身的位置,若是這個子View是一個ViewGroup,除了調用setFrame方法肯定自身的位置外,還要重複onLayout方法中肯定子View位置的過程,最後一層一層的往下,直到所有都子View的layout完成。

二、小結

咱們再來看一下layout的總體流程:從ViewRootImp的performTraversals()方法進入performLayout()方法,開始整顆View樹的佈局流程,在performLayout方法裏面會調用layout方法,咱們發現,View的佈局過程其實也可想測量過程那樣分爲View的layout流程和ViewGroup的layout流程,對於View來講,執行layout方法時只須要直接肯定自身四個頂點的位置便可,而onLayout方法是一個空實現;對於ViewGroup來講,執行layout方法時除了要肯定自身的四個頂點的位置外,那麼它在onLayout方法中還要對本身全部的子View進行layout,最後一層一層的往下,直到所有都layout完成。以下:

layout事後就能夠經過View的getWidth()和getHeight()來獲取最終的寬高的,這個兩個方法的實現以下:

//View.java
public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}
複製代碼

能夠發現就是經過View的四個頂點的差值來獲得View的準確寬高。

View的繪製流程 - performDraw()

和上面兩步類似,View的繪製從ViewRootImpl的performDraw()開始的,以下:

//ViewRootImpl.java
private void performDraw() {
    //...
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    //...
    //一、調用ViewRootImpl的draw方法
    draw(fullRedrawNeeded);
    //...
}
複製代碼

performDraw()方法中並非先調用View的draw方法,而是先調用ViewRootImpl的draw方法,以下:

//ViewRootImpl.java
private void draw(boolean fullRedrawNeeded) {
    //獲取surface繪製表面
    Surface surface = mSurface;
    //...
    //若是surface表面須要更新
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        //判斷是否啓用硬件加速,便是否使用GPU繪製
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            //...
            //使用GPU繪製
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
        }else {
            //...
            //一、調用drawSoftware方法,使用CPU繪製
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
    }
    //...
}
複製代碼

在ViewRootImpl的draw方法中首先獲取須要繪製的區域,而後判斷是否使用GPU進行繪製,使用硬件加速是爲提升了Android系統顯示和刷新的速度,是在在API 11以後引入GPU加速的支持,關於這部分知識可自行查閱資料,不是本文重點,這裏咱們只關心註釋1,一般狀況下咱們使用的是CPU繪製,也就是調用ViewRootImpl的drawSoftware方法來繪製,ViewRootImpl的drawSoftware()方法以下:

//ViewRootImpl.java
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
    final Canvas canvas;
    //...
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        //一、獲取指定區域的Canvas對象,即畫布,用於繪製
        canvas = mSurface.lockCanvas(dirty);
        //...
    }//省略catch
    try {
        //...
        try {
            //...
            //二、從View樹的根節點開始繪製,觸發整顆View樹的繪製
            mView.draw(canvas);
        } finally {
            //...
        }
    } finally {
        try {
            //三、釋放Canvas鎖,而後通知SurfaceFlinger更新這塊區域
            surface.unlockCanvasAndPost(canvas);
        } catch (IllegalArgumentException e) {
           //...
        }
    }
    return true;
}
複製代碼

drawSoftware方法中主要作了3件事:

  • 一、獲取Surface對象並鎖住Canvas繪圖對象
  • 二、從View樹的根視圖開始繪製整顆視圖樹
  • 三、釋放Surface對象並解鎖Canvas,通知SurfaceFlinger更新視圖

一、View和ViewGroup的draw流程

第1和第3點都是操做Surface的基本流程,咱們主要看第二點即註釋2,調用了View的draw方法,它就是一個模板方法,定義了幾個固定的繪製步驟,以下:

//View.java
public void draw(Canvas canvas) {
     final int privateFlags = mPrivateFlags;
     final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */
    //一、繪製背景
    if (!dirtyOpaque) {
        drawBackground(canvas);
     }
    //...
    //二、保存Canvas圖層,爲fadin作準備
    saveCount = canvas.getSaveCount();
    //...
    //三、 繪製自身內容,setWillNotDraw()能夠控制dirtyOpaque這個標誌位
    if(!dirtyOpaque) onDraw(canvas);
    //四、若是是ViewGroup,繪製子View
    dispatchDraw(canvas);
    //...
    //五、若是須要的話,繪製View的fading邊緣並恢復圖層
    canvas.drawRect(left, top, right, top + length, p);
	//...
    //六、繪製裝飾,如滾動條
    onDrawForeground(canvas);
}
複製代碼

你看那英文註釋,它已經替咱們把draw方法中的6大步驟寫出來了,其中最重要的就是註釋3和4,咱們分別來介紹一下:

  • onDraw(canvas):onDraw方法是用來繪製自身內容,若是你的自定義View或ViewGroup須要繪製內容,就要重寫這個方法在Canvas上繪製自身內容。
  • dispatchDraw(canvas):若是是ViewGroup,除了繪製自身內容外,還須要繪製子View的內容,因此dispatchDraw就是把View的繪製一層一層的傳遞下去,直到整顆View樹繪製完畢,ViewGroup重寫了該方法,咱們看一下它的主要源碼以下:
//ViewGroup.java
@Override
protected void dispatchDraw(Canvas canvas) {
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    //...
    for (int i = 0; i < childrenCount; i++) {
        //...
        //若是子View可見
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            //調用drawChild方法,見下面
            more |= drawChild(canvas, child, drawingTime);
        }
    }
}

//ViewGroup.java
 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
     //仍是調用了View的draw方法
     return child.draw(canvas, this, drawingTime);
 }
複製代碼

能夠看到,dispatchDraw方法把繪製子View的任務經過drawChild方法分發給它的子View,若是是一個ViewGroup,又會重複dispatchDraw()過程。

二、onDraw()繪製開關 - setWillNotDraw()

//View.java
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
複製代碼

可是若是你不須要繪製任何內容,你能夠經過View的setWillNotDraw(true)方法關閉繪製,在默認狀況下,View沒有啓用這個優化標誌位,可是ViewGroup會啓用,因此當你的自定義ViewGroup須要經過onDraw來繪製內容時,須要顯式的打開這個開關setWillNotDraw(false),當你的自定義View不須要onDraw來繪製內容時,須要顯式的關閉這個開關setWillNotDraw(true)

三、小結

到這裏,咱們走完了View的繪製過程,咱們再來看一下draw的總體流程:從ViewRootImp的performTraversals()方法進入performDraw()方法,開始整顆View樹的繪製流程,在performDraw()方法中通過層層調用:ViewRootImpl :: draw() -> ViewRootImpl :: drawSoftware() -> View :: draw(),來到View的draw()方法,它裏面定義了View繪製的6大步驟,其中對於View來講,直接調用onDraw()方法繪製自身,對於ViewGroup來講,還要經過dispatchDraw()把繪製子View的流程分發下去,一層層傳遞,直到全部View都繪製完畢。如圖:

總結

咱們一直講View的工做原理,但有沒有發現ViewRootImpl也出現的很頻繁,它雖然不是一個View,但它是鏈接View和Window之間的紐帶,View三大工做流程的起點就是ViewRootImpl的performTraversals()方法,performTraversals()中依此調用了performMeasure() -> performLayout() -> performDraw(),分別對應頂級View的measure、layout和draw流程,而後頂級View的measure流程layout流程又會分別調用咱們熟悉的onMeasure()、onLayout()方法draw流程有點特別,它是經過dispatchDraw()方法來進行draw流程的傳遞, 而onDraw()方法只是單純的繪製自身內容,在onMeasure()方法中會對全部child進行measure過程,同理onLayout()方法中會對全部child進行layout過程,dispatchDraw()方法中會對全部child進行draw過程,如此遞歸直到完成整顆View Hierarchy的遍歷。

該過程如圖:

在閱讀Android源碼時,若是你只是在追蹤方法的調用鏈,這種過程是毫無心義的,可是若是你在這個閱讀過程加入了本身的思考,把它的知識點用本身的語言整理,這樣纔會有所收穫。以上就是我對View的工做原理的理解,但願你們有所收穫。

參考資料:

《Android開發藝術探索》

相關文章
相關標籤/搜索