Android系統分析之View繪製流程與源碼分析

1 概念

  有分析到Activity中界面加載顯示的基本流程原理,記不記得最終分析結果就是下面的關係,id爲content的內容就是整個View樹的結構,所以對每個具體View對象的操作,其實就是個遞歸的實現。
這裏寫圖片描述
  整個View樹的繪圖流程是在ViewRootImpl類的performTraversals()方法開始的(在上一篇博客—-Android系統分析之Window的視圖對象的創建過程分析-的最後階段分析到),該函數做的執行過程主要是根據之前設置的狀態,判斷是否重新計算視圖大小(measure)、是否重新放置視圖的位置(layout)、以及是否重繪 (draw),其核心也就是通過判斷來選擇順序執行這三個方法中的哪個,如下:

private void performTraversals() {
        ......
        //最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
        //lp.width和lp.height在創建ViewGroup實例時等於MATCH_PARENT
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
        ......
        performLayout(lp, mWidth, mHeight);
        ......
        performDraw();
        ......
    }
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
//測Root View,基於它的佈局參數,計算出窗口中根視圖的度量標準
//上面傳入參數後這個函數走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法
//組裝一個MeasureSpec,MeasureSpec的specMode等於EXACTLY,specSize等於windowSize,也就是爲何根視圖總是全屏的原因。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        ......
        }
        return measureSpec;
    }

  其中的mView就是View對象。如下就是整個流程的大致流程圖:
這裏寫圖片描述

2 View繪製流程第一步:遞歸measure源碼分析

2.1 measure源碼分析

這裏寫圖片描述
(1)先看下View的measure方法源碼,如下:

    //final方法,子類不可重寫
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回調onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }

  爲整個View樹計算實際的大小,然後設置實際的高和寬,每個View控件的實際寬高都是由父視圖和自身決定的。實際的測量是在onMeasure方法進行,所以在View的子類需要重寫onMeasure方法,這是因爲measure方法是final的,不允許重載,所以View子類只能通過重載onMeasure來實現自己的測量邏輯。
  在這裏可以看出measure方法最終回調了View的onMeasure方法,我們來看下View的onMeasure源碼,如下:

//View的onMeasure默認實現方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

  測量的結果怎麼辦?onMeasure默認的實現僅僅調用了setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,所以一旦這兩個變量被賦值意味着該View的測量工作結束。
  對於非ViewGroup的View而言,通過調用上面默認的onMeasure即可完成View的測量。如果是ViewGroup就需要測量裏面的所有childview。
  到此一次最基礎的元素View的measure過程就完成了。上面說了View實際是嵌套的,而且measure是遞歸傳遞的,所以每個View都需要measure。實際能夠嵌套的View一般都是ViewGroup的子類,所以在ViewGroup中定義了measureChildren, measureChild, measureChildWithMargins方法來對子視圖進行測量,measureChildren內部實質只是循環調用measureChild,measureChild和measureChildWithMargins的區別就是是否把margin和padding也作爲子視圖的大小。如下我們以ViewGroup中稍微複雜的measureChildWithMargins方法來分析:

//對父視圖提供的measureSpec參數結合自身的LayoutParams參數進行了調整,然後再來調用child.measure()方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //獲取子視圖的LayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //調整MeasureSpec
        //通過這兩個參數以及子視圖本身的LayoutParams來共同決定子視圖的測量規格
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //調運子View的measure方法,子View的measure中會回調子View的onMeasure方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

2.2 measure原理總結

  通過上面分析可以看出measure過程主要就是從頂層父View向子View遞歸調用view.measure方法(measure中又回調onMeasure方法)的過程。具體measure核心主要有如下幾點:

  • MeasureSpec(View的內部類)測量規格爲int型,值由高2位規格模式specMode和低30位具體尺寸specSize組成。其中specMode只有三種值:
MeasureSpec.EXACTLY //確定模式,父View希望子View的大小是確定的,由specSize決定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依據子View的設計值來決定; 
  • View的measure方法是final的,不允許重載,View子類只能重載onMeasure來完成自己的測量邏輯。
  • 最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法確定的(LayoutParams寬高參數均爲MATCH_PARENT,specMode是EXACTLY,specSize爲物理屏幕大小)。
  • ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法,簡化了父子View的尺寸計算。
  • 只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams,否則無法使用layout_margin參數。
  • View的佈局大小由父View和子View共同決定。
  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之後被調用才能返回有效值。

3 View繪製流程第二步:遞歸layout源碼分析

3.1 layout源碼分析

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    //layout方法接收四個參數,這四個參數分別代表相對Parent的左、上、右、下座標。而且還可以看見左上都爲0,右下分別爲上面剛剛測量的width和height。
    view.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}

  類似measure過程,lauout調運了onLayout方法。

3.2 layout原理總結

  整個layout過程比較容易理解,從上面分析可以看出layout也是從頂層父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子View所得到的佈局大小和佈局參數,將子View放在合適的位置上。
  具體layout核心主要有以下幾點:  

  • View.layout方法可被重載,ViewGroup.layout爲final的不可重載,ViewGroup.onLayout爲abstract的,子類必須重載實現自己的位置邏輯。
  • measure操作完成後得到的是對每個View經測量過的measuredWidth和measuredHeight,layout操作完成之後得到的是對每個View進行位置分配後的mLeft、mTop、mRight、mBottom,這些值都是相對於父View來說的。
  • 使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之後被調用才能返回有效值。

4 View繪製流程第三步:遞歸draw源碼分析

4.1 draw源碼分析

private void performDraw() {
    draw(fullRedrawNeeded);
}
public void draw(Canvas canvas) {
        ......
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         * 1. Draw the background,對View的背景進行繪製
         * 2. If necessary, save the canvas' layers to prepare for fading
         * 3. Draw view's content,對View的內容進行繪製
         * 4. Draw children,對當前View的所有子View進行繪製,如果當前的View沒有子View就不需要進行繪製
         * 5. If necessary, draw the fading edges and restore layers
         * 6. Draw decorations (scrollbars for instance),對View的滾動條進行繪製
         */

        // Step 1, draw the background, if needed
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ......

        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        ......
    }

5 View的invalidate和postInvalidate方法源碼分析

  在上面分析View的三步繪製流程中,高頻率調用一個叫invalidate的方法,我們下面對此進行分析。View調用invalidate方法的實質是層層上傳到父級,直到傳遞到ViewRootImpl後觸發了scheduleTraversals方法,然後整個View樹開始重新按照上面分析的View繪製流程進行重繪任務。

5.1 區別

(1)invalidate():在主線程當中刷新;
(2)postInvalidate():在子線程當中刷新;其實最終調用的就是invalidate。原理依然是通過子線程向主線程發送消息這一機制。

5.2 invalidate方法源碼分析

(1)View的invalidate(invalidateInternal)方法

 /**
     * Mark the area defined by dirty as needing to be drawn. If the view is
     * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point in the future.
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     * <b>WARNING:</b> In API 19 and below, this method may be destructive to {@code dirty}.
     * @param dirty the rectangle representing the bounds of the dirty region
     */
     //public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對局部View
    public void invalidate(Rect dirty) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //實質還是調運invalidateInternal方法
        invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
                dirty.right - scrollX, dirty.bottom - scrollY, true, false);
    }

    /**
     * Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The
     * coordinates of the dirty rect are relative to the view. If the view is
     * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point in the future.
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
     //看見上面註釋沒有?public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對局部View
    public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //實質還是調運invalidateInternal方法
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }

    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in the future.
     * This must be called from a UI thread. To call from a non-UI thread, call
     */
     //public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對整個View
    public void invalidate() {
        //invalidate的實質還是調運invalidateInternal方法
        invalidate(true);
    }

    /**
     * This is where the invalidate() work actually happens. A full invalidate()
     * causes the drawing cache to be invalidated, but this function can be
     * called with invalidateCache set to false to skip that invalidation step
     * for cases that do not need it (for example, a component that remains at
     * the same dimensions with the same content).
     *
     * @param invalidateCache Whether the drawing cache for this view should be
     *            invalidated as well. This is usually true for a full
     *            invalidate, but may be set to false if the View's contents or
     *            dimensions have not changed.
     */
     //default的權限,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的纔有效,回調onDraw方法,針對整個View
    void invalidate(boolean invalidateCache) {
        //實質還是調運invalidateInternal方法
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    //!!!!!!看見沒有,這是所有invalidate的終極調用方法!!!!!!
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        ......
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                //設置刷新區域
                damage.set(l, t, r, b);
                //傳遞調運Parent--ViewGroup的invalidateChild方法
                p.invalidateChild(this, damage);
            }
            ......
    }

(2)ViewGroup的invalidateChild方法
  View的invalidate(invalidateInternal)方法實質是將要刷新區域直接傳遞給了父ViewGroup的invalidateChild方法,在invalidate中,調用父View的invalidateChild,這是一個從當前向上級父View回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集 。所以我們看下ViewGroup的invalidateChild方法,源碼如下:

public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;
        final AttachInfo attachInfo = mAttachInfo;
        ......
        do {
            ......
            if (parent instanceof ViewRootImpl) {
            }
            //循環層層上級調用,直到ViewRootImpl會返回null
            parent = parent.invalidateChildInParent(location, dirty);
            ......
        } while (parent != null);
    }

(3)ViewRootImpl的invalidateChildInParent方法
  這個過程最後傳遞到ViewRootImpl的invalidateChildInParent方法結束,所以我們看下ViewRootImpl的invalidateChildInParent方法,如下:

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    //invalidate()
  invalidate();

    return null;
}

  這個ViewRootImpl類的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中說的,層層上級傳遞到ViewRootImpl的invalidateChildInParent方法結束了那個do while循環。

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        //scheduleTraversals()
        scheduleTraversals();
    }
}

  scheduleTraversals會通過Handler的Runnable發送一個異步消息,調運doTraversal方法,然後最終調用performTraversals()執行重繪。
這裏寫圖片描述

5.3 postInvalidate方法源碼分析

(1)View的postInvalidate()方法

public void postInvalidate() {
    //postInvalidateDelayed()
    postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        //核心,實質就是調運了ViewRootImpl.dispatchInvalidateDelayed方法
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

(2)ViewRootImpl類的dispatchInvalidateDelayed方法

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

  通過ViewRootImpl類的Handler發送了一條MSG_INVALIDATE消息,繼續追蹤這條消息的處理可以發現,實質就是又在UI Thread中調運了View的invalidate();方法。

public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        //invalidate()
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}

5.4 invalidate與postInvalidate方法總結

  invalidate系列方法請求重繪View樹(也就是draw方法),如果View大小沒有發生變化就不會調用layout過程,並且只繪製那些“需要重繪的”View,也就是哪個View(View只繪製該View,ViewGroup繪製整個ViewGroup)請求invalidate系列方法,就繪製該View。
(1)依據上面對View的invalidate分析我總結繪製如下流程圖:
這裏寫圖片描述
(2)依據上面對View的postInvalidate分析我總結繪製如下流程圖:
這裏寫圖片描述
(3)常見的引起invalidate方法操作的原因主要有:

  • 直接調用invalidate方法.請求重新draw,但只會繪製調用者本身。
  • 觸發setSelection方法。請求重新draw,但只會繪製調用者本身。
  • 觸發setVisibility方法。 當View可視狀態在INVISIBLE轉換VISIBLE時會間接調用invalidate方法,繼而繪製該View。當View的可視狀態在INVISIBLE\VISIBLE 轉換爲GONE狀態時會間接調用requestLayout和invalidate方法,同時由於View樹大小發生了變化,所以會請求measure過程以及draw過程,同樣只繪製需要“重新繪製”的視圖。
  • 觸發setEnabled方法。請求重新draw,但不會重新繪製任何View包括該調用者本身。
  • 觸發requestFocus方法。請求View樹的draw過程,只繪製“需要重繪”的View。

6 相關問答題

6.1 requestlayout, onlayout, onDraw, drawChild區別與聯繫

(1)requestLayout()方法:會導致調用Measure()方法和layout(),將會根據標誌位判斷是否需要onDraw();
(2)onLayout():擺放viewGroup裏面的子控件;
(3)onDraw():繪製視圖本身;(ViewGroup還需要繪製裏面的所有子控件)
(4)drawChild(): 重新回調每一個子視圖的draw方法,child.draw(canvas, this, drawingTime);

6.2 LinearLayout對比RelativeLayout(實質是性能對比)

(1)RelativeLayout會對子View做兩次measure。這是爲什麼呢?首先RelativeLayout中子View的排列方式是基於彼此的依賴關係,而這個依賴關係可能和佈局中View的順序並不相同,在確定每個子View的位置的時候,就需要先給所有的子View排序一下。又因爲RelativeLayout允許A,B 2個子View,橫向上B依賴A,縱向上A依賴B。所以需要橫向縱向分別進行一次排序測量。
(2)LinearLayout,如果不使用weight屬性,LinearLayout會在當前方向上進行一次measure的過程,如果使用weight屬性,LinearLayout會避開設置過weight屬性的view做第一次measure,完了再對設置過weight屬性的view做第二次measure。由此可見,weight屬性對性能是有影響的,而且本身有大坑,請注意避讓。
(3)參考鏈接:Android中RelativeLayout和LinearLayout性能分析

7 轉載鏈接

Android應用層View繪製流程與源碼分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章