我奶奶都能懂的UI繪制流程(下)!

1.前言

上回咱們說到ViewRootImpl.performTraversals()這個方法,從這里開始,會進入真正的View的繪制流程。第一次看的同學(xué)先去隔壁我奶奶都能懂的UI繪制流程(上)!汲取預(yù)備知識,剩下的同學(xué)系好安全帶,發(fā)車啦!

2.Measure

2.1MeasureSpec

我們從performTraversals()開始參觀,發(fā)現(xiàn)一上來就有個叫childWidthMeasureSpec的玩意兒

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);   
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

從名字上可以猜測,這是用來在測量時確定測量方式的。啥意思?在Android中,控件的大小有三種選擇方式,match_parent,wrap_content以及具體的值。想一想,你叫系統(tǒng)wrap_content,它哪知道該怎么wrap_content,content有多大?父布局給它的空間又有多大?

所以這時候就需要MeasureSpec出場了。

在Measure流程中,系統(tǒng)將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的MeasureSpec,在onMeasure中根據(jù)這個MeasureSpec來確定view的測量寬高。

下面來具體看看這個類

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        ...

MeasureSpec是一個32位的int型數(shù)值,高2位表示mode,低30位表示size。

可以很清晰的看到,MeasureSpec有以下三種類型

1.EXACTLY :父容器已經(jīng)測量出所需要的精確大小,這也是childview的最終大小
——match_parent,精確值
                
2.ATMOST : child view最終的大小不能超過父容器的給的大小
——wrap_content 
                
3.UNSPECIFIED: 不確定,源碼內(nèi)部使用
——一般在ScrollView,ListView 

但是開頭的MODE_MASK = 0x3 << MODE_SHIFT又是什么鬼?

這就涉及到與或非操作了,這玩意兒不是給人看的,這句話沒毛病,他們是給計算機看、以及程序員看的,程序員真的不是人。

從或("|")操作開始,在這里是用來將mode與size結(jié)合起來。

位運算或.png

接著看與("&"),MODE_MASK的作用就類似于網(wǎng)絡(luò)中的掩碼,是用來將內(nèi)容過濾出來的,此處是用來獲取mode。

位運算與.png

最后看與非("~&"),跟上面的與操作類似也是過濾內(nèi)容,這里是用來將size過濾出來

位運算與非.png

到這里還是懵逼的道友,建議你們?nèi)W(xué)習(xí)下計算機組成原理相關(guān)的知識,在這里推薦下《程序是怎樣跑起來的》(日)矢澤久雄著,感覺很棒。大家放心閱讀,我沒有淘寶鏈接。

2.2.getRootMeasureSpec

現(xiàn)在來看看int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width),我們需要注意第二個參數(shù),找到他的起源:

 WindowManager.LayoutParams lp = mWindowAttributes;
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            ...
        }

從上面的代碼可以看到,lp會在初始化時調(diào)用父類的構(gòu)造函數(shù),其默認值是LayoutParams.MATCH_PARENT。現(xiàn)在回到getRootMeasureSpec(mWidth, lp.width)中,查閱這個方法完整的代碼

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;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看到,子View的MeasureSpec是由父View的LayoutParams決定的,這里一共有三種類型,驗證了之前對MeasureSpec的總結(jié)。

而此時我們傳進來的參數(shù)為LayoutParams.MATCH_PARENT,因此返回的childWidthMeasureSpec就是MeasureSpec.EXACTLY。

2.3.View.measure()

準備好了參數(shù),下面就來看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec),該方法會調(diào)用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),這個mView就是decorview,因此最終會調(diào)用view的measure()方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        ...
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        ...

        if (forceLayout || needsLayout) {
            ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

             ...
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

這個方法比較長,我們從上到下慢慢看。

首先看optical,這個就是光陰效果,在測量時,陰影也是要占空間的 。

接著看mMeasureCache,這是用來處理測量緩存的。結(jié)合后面的代碼可以看到,測量前會先嘗試著從mMeasureCache取出緩存,測量后又會將測量結(jié)果放置到緩存中。

最后onMeasure(widthMeasureSpec, heightMeasureSpec)是重點。在整個measure()方法中,我們并沒有看到具體的測量代碼,因為不同的View其測量方法也是不同的,需要由子類自己去決定。

這是一個典型的模板方法模式(設(shè)計模式之一,以后將Android架構(gòu)的時候填坑),其中measure()是模板,由于所有控件最終都是繼承自View的,因此只需要在View中實現(xiàn)measure()就可以了;而onMeasure()則需要由子View自定義,因此子View會重寫onMeasure()方法。

2.4.View.onMeasure()

在閱讀decorview的onMeasure()之前,我們先來看看View的onMeasure()方法

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

很簡單,只是調(diào)用了setMeasuredDimension

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

可以看到這里也對光陰效果進行了處理,最后調(diào)用setMeasuredDimensionRaw()

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

在這里,我們對mMeasuredWidth以及mMeasuredHeight進行賦值。

有沒有想起來什么?以前大家會在onCreate()方法中通過getMeasuredXXX()來獲取控件的寬高,結(jié)果失敗了,為什么?以getMeasuredHeight()為例

public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

這里會返回mMeasuredHeight ,而mMeasuredHeight是在onResume()中通過ViewRootImpl進行一系列復(fù)雜的調(diào)用最終在View的setMeasuredDimensionRaw()中被賦值,所以在onCreate()中自然是獲取不到的。

回到上面的方法中,在默認的情況下,這個measuredWidthmeasuredHeight又是哪來的呢?我們來看看getSuggestedMinimumWidth()做了什么

  protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

這個mMinWidth得記住它,在自定義控件時是很關(guān)鍵的一個數(shù)值。一般都需要為其賦值,可以通過代碼與XML兩種方式。

2.5.FrameLayout.onMeasure()

說了一大堆廢話,現(xiàn)在我們回去看看DecorView的onMeasure()方法。

遺憾的是,這里面也沒做具體的測量行為,反而是調(diào)用了super.onMeasure(widthMeasureSpec, heightMeasureSpec),也就是FrameLayout的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);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                ...
                }
            }
        }
        ...
}

循環(huán)前獲取了子View的數(shù)量,接著開始對每一個子View進行測量以獲取其測量寬高。主要就是通過measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)方法。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

是不是又有點熟悉?對啦,開始測量時,performTraversals()也是這樣做的。

首先獲取childWidthMeasureSpec以及childHeightMeasureSpec,然后通過child.measure(childWidthMeasureSpec, childHeightMeasureSpec)完成測量。很明顯這是一個迭代的過程。

不同的是,performTraversals()獲取到的是根節(jié)點的MeasureSpec,而這里要獲取的是子View的MeasureSpec,因此要考慮的父View與本身兩個因素.

2.5.1.MeasureSpec九兄弟

我們來看看getChildMeasureSpec(),一共有3*3=9種情況。文中我只介紹其中3種的代碼

  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        ...
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

代碼結(jié)構(gòu)很清楚,首先判斷父View的MeasureSpec,如果是MeasureSpec.EXACTLY,則開始判斷子ViewchildDimension

1.如果是具體的>0點值,就直接將這個值賦給子View,并將類型設(shè)置為MeasureSpec.EXACTLY;
2.如果是LayoutParams.MATCH_PARENT,則將值設(shè)置為父容器的大小,類型為MeasureSpec.EXACTLY;
3.如果是LayoutParams.WRAP_CONTENT,則將值設(shè)置為父容器的大小,類型為 MeasureSpec.AT_MOST;

剩下的6種情況也是類似的,代碼就不展示了,直接上總結(jié)的圖片

getChildMeasureSpec方法分析.png

這張表不用刻意去記,先想一想,你會發(fā)現(xiàn)確實是這么一回事兒。

2.6 Measure總結(jié)

子View的測量在measureChildWithMargins()中也終于搞定,說了這么多,UI繪制的第一步measure終于差不多了,我們來總結(jié)下吧。
1、View的測量
onMeasure方法里面調(diào)用setMeasuredDimension()確定當前View的大小
2.、ViewGroup的測量
2.1、遍歷測量Child,可以通過下面三個方法來遍歷測量:ChildmeasureChildWithMargins、measureChild、measureChildren
2.2、setMeasuredDimension 確定當前ViewGroup的大小

View樹的源碼measure流程圖.png

3.Layout

前面花了那么大的篇幅介紹Measure過程,現(xiàn)在回過頭再來看Layout就會比較簡單,因為他們的套路都是一樣的。

performLayout()開始,直接調(diào)用layout()方法,簡潔明了

final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

其中host就是我們的decorview,來看看最關(guān)鍵的layout()方法

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

      ...
    }

一開始,先要根據(jù)flag判斷是否需要再次measure。

接著,將左上右下的位置依次賦值給oldL、oldT、oldR、oldB

繼續(xù),boolean changed的作用和measure中cache相似,是用來減少布局操作的。這兒是個三目運算符,根據(jù)有無光影調(diào)用不同的方法,我們以setFrame(l, t, r, b)為例

  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;
            ...
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            ...
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
          ...
        }
        return changed;
    }

怎么樣,是不是相當?shù)耐ㄋ滓锥??就是十分常見的緩存策略?/p>

最后,layout()負責調(diào)用onLayout(changed, l, t, r, b),用志玲姐姐的話來說,“這是一個空箱子”

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

再去看看ViewGroup的onLayout(),更絕了,是個抽象方法。也就是說,每一個View的onLayout()都需要自己去實現(xiàn)。想想也是這個道理,自己想成為什么樣的人,不是自己說了算嗎?
在這里,為了觀影體驗,我們以FrameLayout為例,看看他的onLayout()做了什么

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

一上來就是為子View布局

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        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;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
               ...
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

layoutChildren()本身也很簡單明了,獲取子控件數(shù)量,然后循環(huán),依次獲取寬高,判斷各種情況,并調(diào)用子控件的布局方法。

沒錯就是這么簡單,Layout也吹完了。

4.Draw

有了Measure和Layout的基礎(chǔ),Draw理解起來就更加簡單了。
按照國際慣例,我們從ViewRootImpl.performDraw()看起

private void performDraw() {
 ...
draw(fullRedrawNeeded);
...

ViewRootImpl.draw()又會調(diào)用ViewRootImpl.drawSoftware(),然后調(diào)用mView.draw(canvas)。我們知道m(xù)View就是DecorView,而這個方法最終就會走進到View.draw()方法中。

public void draw(Canvas canvas) {
        ...
        /*
         * 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)
         */

太貼心了,這些注釋已經(jīng)說明了一切。

挑重點的說,先看第一步,繪制背景。

 // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

這里有一個叫dirtyOpaque的標志。在自定義ViewGroup時,一般是不會調(diào)用onDraw方法的,除非設(shè)置了background。仔細想想這也是理所當然的,我沒有背景,有什么好畫的。這也是產(chǎn)生過度繪制的原因之一。

稍微拓展一下,為什么說LinearLayout比RelativeLayout繪制快?其實他們在measure和layout上所花的時間是差不多的,區(qū)別就在于draw,RelativeLayout要從左右、上下兩個方向繪制,而LinearLayout只需要繪制一次。

第三步繪制內(nèi)容也是同理,一般ViewGroup本身都不會有內(nèi)容,有的只是childView。

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

最后看下繪制子View,這是個空方法,留給后人繼承。

/**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }

我們一般不會和他打招呼,draw更多的是應(yīng)用在自定義View中,也就是說只要重寫onDraw()方法即可。

到此為止,Draw也說完了,整個UI繪制結(jié)束!

5.實踐是檢驗真理的唯一標準

說完原理,我們來看一個應(yīng)用。

5.1 ScrollView與ListView不兼容問題

眾所周知,在ScrollView中嵌套ListView時,ListView只會顯示第一行,這是為什么呢?讓我們一起走進ListView.onMeasure()的內(nèi)心世界

if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

這是啥?在heightMode 為MeasureSpec.UNSPECIFIED時,ListView的高度竟然就只測量第一個childView的高度!

再來看看ScrollView,他重寫了measureChildWithMargins(),有這么一句話

final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

搜滴寺內(nèi)!ScrollView會將子View的HeightMeasureSpec設(shè)置為MeasureSpec.UNSPECIFIED,于是ListView到了這里就懵逼了。

至于解決方法的話,重寫ListView或者ScrollView都可以,是不是感覺思路很清晰?

6.總結(jié)

真的連你奶奶都可以懂的!

完結(jié)撒花~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容