深入 Activity 三部曲(3)之 View 繪制流程

UI 優(yōu)化系列專題,來聊一聊 Android 渲染相關知識炮温,主要涉及 UI 渲染背景知識火脉、如何優(yōu)化 UI 渲染兩部分內容。


UI 優(yōu)化系列專題
  • UI 渲染背景知識

View 繪制流程之 setContentView() 到底做了什么柒啤?
View 繪制流程之 DecorView 添加至窗口的過程
深入 Activity 三部曲(3)View 繪制流程
Android 之 LayoutInflater 全面解析
關于渲染倦挂,你需要了解什么?
Android 之 Choreographer 詳細分析

  • 如何優(yōu)化 UI 渲染

Android 之如何優(yōu)化 UI 渲染(上)
Android 之如何優(yōu)化 UI 渲染(下)


在 View 繪制流程系列白修,分別介紹了 View 的創(chuàng)建以及添加至窗口的過程妒峦,它們也是為今天要分析的 View 繪制任務做的鋪墊重斑,View 的繪制流程主要包含三個階段:measure -> layout -> draw兵睛。

在具體分析之前,還是通過幾個問題來了解下今天要分析的內容:

  • Handler 異步消息的作用?
  • Android 是如何解決不確定的布局尺寸祖很?即 MATCH_PARENT 或 WRAP_CONTENT笛丙。
  • 為什么 View.GONE 不會占用布局空間?
  • getWidth() 和 getMeasuredWidth() 有什么區(qū)別假颇?在什么時候調用才會有值胚鸯?

requestLayout()

View 繪制的起始點是在 ViewRootImpl 的 requestLayout 方法,前面有分析到在該方法首先會檢查是否在原線程笨鸡。這里簡單說下姜钳,UI 的繪制并非一定要在主線程,但是它要求是在原線程形耗,絕大多數(shù)操作系統(tǒng) UI 框架都是單線程的哥桥,這主要是因為多線程的 UI 框架在設計上會非常復雜。

然后通過 scheduleTraversals 方法發(fā)送消息開始 View 繪制流程:

 void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //編舞者激涤,可以用它來監(jiān)聽幀頻
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //... 省略
    }
}

注意 postSyncBarrier() 發(fā)送同步屏障消息拟糕,可能很多人不知道 Handler 有兩種 Message 類型。

  • 同步消息(普通消息)
  • 異步消息(Android 4.1 新增倦踢,配合 VSYNC 信號)

Handler 的構造方法提供了用于區(qū)分兩種消息的構造方法送滞,不過它們被 @hide 了,但是 Message 為我們敞開了:

//設置為異步消息
public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

//獲取當前消息類型辱挥,是同步消息還是異步消息
public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}

一般情況下同步消息和異步消息的處理方式并沒有什么區(qū)別犁嗅,只有在設置了同步屏障時才會出現(xiàn)差異。同步屏障為 Handler 消息機制增加了一種簡單的優(yōu)先級關系晤碘,異步消息的優(yōu)先級要高于同步消息愧哟,用于配合系統(tǒng)的 VSYNC 信號。簡單點說哼蛆,設置了同步屏障之后蕊梧,Handler 只會處理異步消息。

但是發(fā)送同步屏障的接口并沒有對應用開發(fā)者公開腮介,其實它的主要作用是為了更快的響應 UI 繪制事件肥矢,避免長時間等待于消息隊列。

繼續(xù)分析叠洗,發(fā)送 UI 繪制任務 mTraversalRunnable 到 Choreographer甘改。

//編舞者,可以用它來監(jiān)聽幀頻
mChoreographer.postCallback(
   Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
  1. Choreographer 是負責獲取 VSYNC 同步信號并統(tǒng)一調度 UI 的繪制任務灭抑。Choreographer 是線程級別單例十艾,并且具有處理當前線程消息隊列(MessageQueue)的能力。關于 Choreographer 更詳細的分析腾节,可以參考《Android 之 Choreographer 詳細分析》忘嫉。
// 線程級別單例荤牍,肯定不會感到默認,最簡單的方式使用ThreadLocal
private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        // 當前線程Looper庆冕,當前的分析在主線程
        Looper looper = Looper.myLooper();
        if (looper == null) {
            throw new IllegalStateException("The current thread must have a looper!");
        }
        // 為當前線程創(chuàng)建一個Choreographer
        return new Choreographer(looper, VSYNC_SOURCE_APP);
    }
};

看下 Choreographer 的構造方法康吵,注意 FrameHandler 接收對應線程的 Looper 對象。

private Choreographer(Looper looper) {
    //當前線程Looper
    mLooper = looper;
    //創(chuàng)建handle對象访递,用于處理消息
    mHandler = new FrameHandler(looper);
    //創(chuàng)建VSYNC的信號接受對象
    mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
    //初始化上一次frame渲染的時間點
    mLastFrameTimeNanos = Long.MIN_VALUE;
    //計算幀率晦嵌,也就是一幀所需的渲染時間,getRefreshRate是刷新率拷姿,一般是60
    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
    //創(chuàng)建消息處理隊列
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}
  1. mTraversalRunnable

mTraversalRunnable 本質是一個 Runnable惭载,通過 mChoreographer.postCallback() 發(fā)送到主線程消息隊列(這里以主線程繪制流程做分析)。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //開始執(zhí)行繪制遍歷
        doTraversal();
    }
}

doTraversal() 真正開始執(zhí)行 UI 繪制的遍歷過程:

void doTraversal() {
    if (mTraversalScheduled) {
        //防止重復
        mTraversalScheduled = false;
        //移除屏障消息
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //執(zhí)行UI繪制的遍歷過程
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

mTraversalScheduled 變量主要防止重復的繪制任務响巢,removeSyncBarrier 方法移除同步屏障棕兼,因為此時 View 繪制任務已經(jīng)處于執(zhí)行過程中。performTraversals() 將依次完成 View 的三大繪制流程:performMeasure()抵乓、performLayout() 和 performDraw()伴挚。

private void performTraversals() {
    // 當前DecorView
    final View host = mView;
    // ... 省略
    //想要展示窗口的寬高
    int desiredWindowWidth;
    int desiredWindowHeight;
    if (mFirst) {
        //將窗口信息依附給DecorView
        host.dispatchAttachedToWindow(mAttachInfo, 0);
    }
    //開始進行布局準備
    if (mFirst || windowShouldResize || insetsChanged ||
        viewVisibilityChanged || params != null) {
        // ... 省略
        if (!mStopped) {
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                // DecorView默認LayoutParams的屬性是MATCH_PARENT
                // 此時的寬度測量模式為EXACTLY(表示確定大小), 測量大小為窗口寬度大小灾炭,因為DecorView的LayoutParams為MATCH_PARENT
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                // 高度測量模式也是確定的EXACTLY茎芋,測量大小為窗口高度大小
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                // 執(zhí)行View測量工作,計算出每個View尺寸
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                int width = host.getMeasuredWidth();
                int height = host.getMeasuredHeight();
                boolean measureAgain = false;

                /*******部分代碼省略**********/

                if (measureAgain) {
                    //View的測量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                layoutRequested = true;
            }
        }
    } else {
        /*******部分代碼省略**********/
    }

    final boolean didLayout = layoutRequested /*&& !mStopped*/ ;
    boolean triggerGlobalLayoutListener = didLayout
            || mAttachInfo.mRecomputeGlobalAttributes;
    if (didLayout) {
        //View的布局
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);

        /*******部分代碼省略**********/
    }
    /*******部分代碼省略**********/
    
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            /*******部分代碼省略**********/
            //View的繪制
            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();
        }
    }

    mIsInTraversal = false;
   }
 }

首先需要說明 DecorView 的 LayoutParams 寬高默認為 MATCH_PARENT蜈出,即窗口尺寸田弥。

public LayoutParams() {
      //默認是MATCH_PARENT
      super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
      type = TYPE_APPLICATION;
      format = PixelFormat.OPAQUE;
}

在具體分析測量過程之前,先要講下 Android 系統(tǒng)為自適應布局尺寸引入了 LayoutParams.MATCH_PARENT 和 LayoutParams.WRAP_CONTENT铡原,這樣就會有不確定的情況偷厦,那 Android 又是如何解決不確定的布局尺寸呢?

答案就是 MeasureSpec( 測量規(guī)格)燕刻,它本質是 4 個字節(jié)的 int 數(shù)值只泼,主要包含兩部分:高 2 位表示測量模式,低 30 位表示測量大小卵洗。

  1. 測量模式
  • MeasureSpec.EXACTLY:精確大小请唱,父容器已經(jīng)測量出所需要的精確大小,這也是我們 childView 的最終大小 — MATCH_PARENT过蹂。

  • MeasureSpec.AT_MOST: 最終的大小不能超過我們的父容器 — WRAP_CONTENT十绑。

  • UNSPECIFIED:不確定的,源碼內部使用酷勺,一般在 ScorllView本橙、ListView 中能看到這些,需要動態(tài)測量脆诉。

在 MeasureSpec 中測量模式關鍵方法:

    /**
     * 獲取測量模式甚亭,取最高兩位
     */
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        //MODE_MASK為110000000000000000000000000000
        return (measureSpec & MODE_MASK);
    }
  1. 測量大小
    測量大小是根據(jù)測量模式來確定贷币,在 Measure 流程中,系統(tǒng)將 View 的 LayoutParams 根據(jù)父容器施加的規(guī)則轉化成對應的 MeasureSpec狂鞋,在 onMeasure() 中根據(jù)這個 MeasureSpec 來確定 View 的測量寬高。

在 MeasureSpec 中測量大小關鍵方法:

 /**
  * 獲取測量大小潜的,取低30位
  */
 public static int getSize(int measureSpec) {  
      //~MODE_MASK為00111111111111111111111111111111
       return (measureSpec & ~MODE_MASK);
 }

說道這里骚揍,需要先看下表示窗口視圖 DecorView 的測量模式和測量大小:

// 獲取DecorView的寬高測量規(guī)格
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        // DecorView默認走這里
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 此時測量大小就是窗口大小啰挪,測量模式就是EXACTLY信不,表示確定的
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 此時測量模式可調整的,即AT_MOST(最大)亡呵,最大為窗口大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    //返回測量規(guī)格
    return measureSpec;
}

由于 DecorView 的寬高為 MATCH_PARENT抽活,故它的寬高測量規(guī)格都為:EXACTLY + windowSize(窗口大小,視具體手機屏幕決定)锰什。

1. performMeasure()

接下來開始 View 的測量工作下硕,注意 mView 實際是 DecorView 如下:

 /**
 * 執(zhí)行View的測量工作
 */
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        // mView實際是DecorView
        // 也就是真正測量工作是從DecorView開始的
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

調用 DecorView 的 measure 方法,不過 measure 方法是 View 獨有的汁胆,并且被聲明為 final梭姓。

/**
 * 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);
    }

    // Suppress sign extension for the low bytes
    // 將寬度測量規(guī)格和高度測量規(guī)格整合成long,高32位為寬度嫩码,低32位為高度測量規(guī)格
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    // 緩存當前測量結果的容器
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    // 是否強制布局
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    // 這里主要是做優(yōu)化誉尖,防止在未發(fā)生變化的情況下,無謂的測量工作
    // 寬度和高度測量規(guī)格是否發(fā)生過變化
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    // 寬高的測量模式是否為EXACTLY
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    // 新的測量大小是否等于當前測量大小
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);

    // 如果與上次測量結果發(fā)生變化此時需要重寫測量
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        // < 0 表示當前測量已經(jīng)失效(緩存不存在)铸题,需要重新測量
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            // 否則測量結果未發(fā)生變化铡恕,value高32位為寬度,低32位為高度
            long value = mMeasureCache.valueAt(cacheIndex);
            // Casting a long to int drops the high 32 bits, no mask needed
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            // 注意這個標志位丢间,標記在layout之前需要measure
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

         // 如果自定義View中沒有調用setMeasuredDimension()探熔,會拋出異常。
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    // 保存最新測量規(guī)格
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    // 緩存當前測量結果
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

可以看到系統(tǒng)對 View 的測量工作做了大量的優(yōu)化烘挫,只為有效減少無謂的測量工作祭刚,提高 UI 渲染性能。

  • 注意代碼中 if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET)墙牌,如果我們在自定義 View 過程中涡驮,最后沒有給 View 設置測量大小,即 setMeasuredDimension() 喜滨,此時將會拋出異常捉捅。后面會分析到。

如果需要測量虽风,此時調用 onMeasure 方法棒口,onMeasure 方法的設計與 measure 方法不同寄月,measure 方法在 View 中設計為 final,而 onMeasure 方法旨在子 View 重寫該方法无牵,這也很容易理解漾肮,View 的最終大小需要自行去測量。

  • 注意:onMeasure 方法是需要具體 View 自行實現(xiàn)茎毁,所以在 ViewGroup 中沒有實現(xiàn)該方法克懊。

我們先來看下 View 的默認測量過程 onMeasure 方法如下:

/**
  *  View默認的onMeasure方法
  */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //getSuggestedMinimumWidth確定當前View的最小尺寸乾翔,根據(jù)最小尺寸與背景尺寸取較大值
    //getDefaultSize()確定子View的尺寸
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
  • getSuggestedMinimumXxx()皂甘,從名字看是獲取 View 最小建議尺寸戈毒,以寬度為例:
/**
 * 確定View的最小寬度憎茂,根據(jù)最小寬度和背景寬度取較大值
 */
protected int getSuggestedMinimumWidth() {
    // 沒有背景圖度迂,則使用最小寬度
    // 否則取最小寬度和背景寬度的較大值
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}   
  • getDefaultSize()肝劲,根據(jù)最小建議尺寸和測量大小決定 View 的最終尺寸:
/**
 * size為當前View的最小尺寸
 * measureSpec突雪,當前View的測量規(guī)格
 */
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    // 獲取測量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    // 獲取測量大小
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            //如果測量模式為不確定
            //此時尺寸就是View的最小尺寸
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            //此時為View的測量大小
            result = specSize;
            break;
    }
    return result;
}
  • setMeasuredDimension() 最終執(zhí)行到 setMeasuredDimensionRaw() 設置 View 的測量大猩了簟:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    // 賦值給View成員
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    // 標志碧库,已經(jīng)設置View的測量大小
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

記得上面說到的自定義 View 柜与,如果在其 onMeasure 方法中沒有調用 setMeasureDimension 方法,將會拋出異常嵌灰,此時在方法最后修改該標志位:

// 標志旅挤,已經(jīng)設置View的測量大小
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

注意,文章開頭提出的問題 getMeasuredWidth() / getMeasuredHeight() 在什么時候才會獲取到值伞鲫?答案就在這里粘茄,setMeasuredDimensionRaw 方法執(zhí)行結束,此時調用 View 的 getMeasureXxx()秕脓,便可以拿到 View 的測量大小了柒瓣。

// 獲取測量寬度
public final int getMeasuredWidth() {
    // measuredWidth & 0x00ffffff,舍去高2為測量模式吠架,取低30位測量大小
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

// 獲取測量高度
public final int getMeasuredHeight() {
    // 原理一致
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

即 View 的測量大小在 measure 階段完成之后便可以獲取到芙贫。注意此時 getWidth() / getHeight() 仍然無法爭取獲取傍药!

分析完了 View 的默認測量規(guī)則磺平,但由于 DecorView 繼承自 FrameLayout,所以此時 onMeasure 實際調用的是 FrameLayout 的 onMeasure 方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 獲取子View數(shù)量
    int count = getChildCount();

    // 確定當前View寬高測量模式存在非EXACTLY拐辽,注意這將有可能導致FrameLayout二次測量
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    // 遍歷子View
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 為什么GONE不占用空間就在這里
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 測量子View的大小
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // 獲取子View的LayoutParams拣挪,獲取其他布局參數(shù)
            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());
            //如果當前FrameLayout寬高存在不是EXACTLY。
            if (measureMatchParentChildren) {
                //如果子View存在需要依賴父容器的測量大小
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    //加入需要二次測量
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // 最大寬度累加自身padding
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    // 最大高度累加自身padding
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // 與最小高度取較大值
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    // 與最小寬度取較大值
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    //如果存在foreground drawable
    if (drawable != null) {
        // 判斷與foreground drawable 高度取較大值
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        // 判斷與foreground drawable 寬度取較大值
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

    //設置測量大小
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    /**
     * 1.如果FrameLayout 的寬高測量模式存在非EXACTLY
     * 2.與包含的子View需要依賴父View的測量大小時俱诸,(子View存在MATCH_PARENT)
     * 此時需要二次測量
     * */
    count = mMatchParentChildren.size();
    //此時需要二次測量
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }

            final int childHeightMeasureSpec;
            if (lp.height == LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

注意 measureMatchParentChildren 變量菠劝,它用于標注當前 FrameLayout 寬/高測量模式是否存在非 MeasureSpec.EXACTLY。這會導致 FrameLayout 在測量階段的性能問題 — 二次測量睁搭,后面分析到赶诊。

第一個 for 循環(huán)開始遍歷測量所有子 View笼平,注意條件:

child.getVisibility() != GONE

這就是為什么 View.GONE 不會占用布局空間,View.GONE 在測量階段默認被忽略舔痪。

開始測量子 View 過程寓调,measureChildWithMargins 方法如下:

protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {

    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 子View的寬度測量規(guī)格,根據(jù)父容器施加的規(guī)則锄码,加上寬度內邊距
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    // 子View的高度測量規(guī)格夺英,根據(jù)父容器施加的規(guī)則,加上高度內邊距
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    // 調用子View的measure方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

看下如何確定子 View 的測量規(guī)格巍耗,getChildMeasureSpec 方法如下:

/**
 * 根據(jù)父容器的測量規(guī)格確定子View的測量規(guī)格
 */
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);

    //返回當前View的測量大小
    int resultSize = 0;
    //返回當前View的測量模式
    int resultMode = 0;

    switch (specMode) {
        // Parent has imposed an exact size on us

        case MeasureSpec.EXACTLY:
            //如果子View的尺寸是固定的
            //測量大小就是View設置的具體值childDimension(lp.width)
            //測量模式就是精確的EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                //如果是MATCH_PARENT秋麸,此時表示子View使用父容器尺寸
                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.
                //子View的大小不確定渐排,但是最大不超過父容器大小
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                //此時子View的尺寸也是確定的
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                //此時子View的最大大小為父容器大小
                //測量模式是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                // 子View的尺寸不能確定炬太,但是最大不能超過父容器
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                // 子View的尺寸是確定的
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                //子View需要動態(tài)的測量
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                //子View需要動態(tài)的測量
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    //noinspection ResourceType
    //生成子View的測量規(guī)格
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

前面也有簡單提到子 View 要根據(jù)父 View 施加的測量規(guī)格決定自己的測量大小。關于子 View 的測量規(guī)格這里做下簡單總結:

  1. 如果子 View 的 LayoutParams 為具體數(shù)值驯耻,此時無論父 View 施加測量模式是什么亲族,子 View 的測量規(guī)格都為 EXACTLY + childDimension(在 LayoutParams 中設置的具體數(shù)值)。

  2. 如果子 View 的 LayoutParams 為 MATCH_PARENT可缚,當父 View 的測量模式為 EXACTLY 時霎迫,子 View 的測量規(guī)格為 EXACTLY + 小于等于父 View 的測量大小帘靡;當父 View 的測量模式為 AT_MOST 時知给,子 View 的測量規(guī)格為 AT_MOST + 小于等于父 View 的測量大小描姚;當父 View 的測量大小為 UNSPECIFIED 時涩赢,子 View 的測量規(guī)格為 UNSPECIFIED + 0。

  3. 如果子 View 的 LayoutParams 為 WRAP_CONTENT轩勘,當父 View 的測量模式為 EXACTLY筒扒,子 View 的測量規(guī)格為 AT_MOST + 小于等于父 View 測量大小绊寻;當父 View 的測量規(guī)格為 AT_MOST 時花墩,子 View 的測量規(guī)格為 AT_MOST + 小于等于父 View 的測量大小澄步;當父 View 的測量模式為 UNSPECIFIED 時冰蘑,子 View 的測量規(guī)格為 UNSPECIFIED + 0。

在 measureChildWithMargins 方法最后村缸,調用 View 的 measure 方法完成測量結果懂缕,關于 View 的默認測量流程前面已經(jīng)做過分析,感興趣的朋友可以去分析下例如 ImageView王凑、TextView 的測量過程搪柑。

重新回到 FrameLayout 的 onMeasure 方法聋丝,注意看在第一個 for 循環(huán),如果當前 FrameLayout 的寬 / 高測量模式存在非 EXACTLY(即 measureMatchParentChildren == true)工碾,此時它所包含的子 View 存在 LayoutParams 為 MATCH_PARENT 時弱睦,會將該 View 記錄在 mMatchParentChilden 中。被記錄下的 View 需要二次測量確定大小渊额。

小結
  1. 用一張圖再來了解下 View 的整個測量過程:
View 樹的源碼 measure 流程圖
  1. 用一張表格總結下子 View 的測量規(guī)格:
ParentSpceMode ParentSpceSize ChildDimension ChildSpecMode ChildSpceSize
EXACTLY Size >= 0 EXACTLY ChildDimension
同上 同上 MATCH_PARENT EXACTLY Size
同上 同上 WRAP_CONTENT AT_MOST Size
AT_MOST 同上 >= 0 EXACTLY childDimension
同上 同上 MATCH_PARENT AT_MOST Size
同上 同上 WRAP_CONTENT AT_MOST Size
UNSPECIFIED 同上 >= 0 EXACTLY 0
同上 同上 MATCH_PARENT UNSPECIFIED 0
同上 同上 WRAP_CONTENT UNSPECFIFE 0
  1. 應盡可能避開在使用 FrameLayout 時發(fā)生二次測量况木。
  • 確定大小的 FrameLayout,即就是保證 FrameLayout 的測量模式為 MeasureSpec.EXACTLY旬迹。

  • 確定大小的 ChildView火惊,或者使用 WRAP_CONTENT 。

至此 View 的測量過程就分析完了奔垦,不過測量過程涉及的細節(jié)內容非常多屹耐,感興趣的朋友可以繼續(xù)深入分析。

measure 階段實際就是確定 View 的大小椿猎,那接下來的 layout 階段就要開始擺放 View 的在容器中的位置了惶岭。


2. performLayout()

相比起 View 的測量過程,布局階段可能相對簡單一些犯眠。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                           int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;

    // mView是DecorView
    final View host = mView;
    if (host == null) {
        return;
    }

    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    // layout就是確定View的擺放位置
    try {
        // host為DecorView
        // 由于DecorView的LayoutParams為MATCH_PARENT,故它的left和top都為0
        // 獲取到DecorView的測量寬度按灶,left + 測量寬度即 right
        // 獲取到DecorView的測量高度,top + 測量高度即 bottom
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

        // ... 省略
        
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

注意 host 仍然是 DecorView筐咧,layout 方法與 measure 方法在 View 中的策略類似鸯旁,不過 layout 方法并沒有被聲明為 final。

// View 的 layout
public void layout(int l, int t, int r, int b) {

    // layout之前需要先進行measure測量工作
    // 注意前面分析measure階段量蕊,如果當前需要測量铺罢,但是發(fā)現(xiàn)已經(jīng)緩存了該測量結果時,measure階段
    // 并沒有真正執(zhí)行onMeasure危融,只是將mPrivateFlags3標記為PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
    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;

    // 根據(jù)是否有光影效果
    // changed標志View坐標是否發(fā)生變化
    // setFrame 保存新的坐標位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 這也是做一層優(yōu)化畏铆,避免無謂的遍歷layout過程
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 執(zhí)行布局擺放,當前實際是 DecorView吉殃,這里實際調用了FrameLayout的onLayout
        onLayout(changed, l, t, r, b);

        if (shouldDrawRoundScrollbar()) {
            if (mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        // 回調OnLayoutChange辞居,表示當前布局坐標發(fā)生新的變化
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

    if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
        mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
        notifyEnterOrExitForAutoFillIfNeeded(true);
    }
}

changed 變量同樣是避免無謂的 layout 操作,這里重點看下 setFrame 方法(setOpticalFrame() 最終也是調用了 setFrame()):

protected boolean setFrame(int left, int top, int right, int bottom) {
    // 標志View坐標是否真的發(fā)生變化
    boolean changed = false;

    // 判斷View的坐標信息是否發(fā)生變化
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        // 表示當前View坐標發(fā)生變化
        changed = true;

        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;

        // 原寬度
        int oldWidth = mRight - mLeft;
        // 原高度
        int oldHeight = mBottom - mTop;
        // 新寬度
        int newWidth = right - left;
        // 新高度
        int newHeight = bottom - top;
        // View大小是否發(fā)生變化蛋勺,
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);

        // 保存View最后坐標位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        if (sizeChanged) {
            // View的sizeChange方法被回調
            // 注意如果僅是坐標位置發(fā)生變化瓦灶,View自身尺寸未發(fā)生變化sizeChange是不會被調用的
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        // ... 省略
    }
    return changed;
}

setFrame 方法就是為 View 賦值新的 left、right抱完、top 和 bottom 四個坐標點贼陶,View 的坐標位置真正發(fā)生變化, changed 變量才會返回 true。

  • 注意看 sizeChanged 變量碉怔,只有當 View 的寬 / 高發(fā)生變化才會回調 sizeChange 方法烘贴。

這里還需要重點關注下 View 的寬 / 高獲取,注意結合上面 View 的四個坐標點:

// 獲取View的寬度撮胧,right - left 即 View 寬度
public final int getWidth() {
    return mRight - mLeft;
}

// 獲取View的高度桨踪,bottom - top 即 View 高度
public final int getHeight(){
    return mBottom - mTop;
}

也就是說 View 的 getWidth() / getHeight() 是在 layout 階段完成之后,才能夠正確獲取值芹啥。

大家肯定有過這樣的疑問锻离,如何能在 Activity 的 onCreate 方法獲取到 View 的寬高呢?要知道此時 View 的繪制流程還未開始墓怀,這里推薦 2 種思路供大家參考汽纠。

  1. ViewTreeObserver
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
          // performLayout方法執(zhí)行結束之后,回調
          // 此時表示所有的View都已經(jīng)布局完成傀履,可以獲取到任何View組件的寬度虱朵、高度、左邊啤呼、右邊等信息
          // 需要注意多次調用帶來的影響
       }
 });

performLayout 方法執(zhí)行完畢卧秘,此時所有的 View 已經(jīng)布局完成呢袱,便會回調 onGlobalLayout 通知官扣。

  1. view.post()

相信很多人都使用過該方法,并且知道任務會被添加到主線程(當前分析主線程渲染)消息隊列等待執(zhí)行羞福;但是它背后的執(zhí)行原理可能大多數(shù)開發(fā)者并不一定了解惕蹄,簡單來說,它保證了在 View 繪制流程結束后回調相關任務治专,此時我們就可以正確獲取到 View 的寬高了卖陵。具體你可以參考《Android 之你真的了解 View.post() 原理嗎?

重新回到 DecorView 的 layout 方法张峰,如果需要布局則調用 onLayout 方法泪蔫,onLayout() 在 View 中默認為空實現(xiàn),但是在 ViewGroup 將其重寫為 abstract喘批,即強制 ViewGroup 的子類重寫該方法撩荣,因為布局容器必須實現(xiàn) childView 的布局擺放任務。

DecorView 繼承自 FrameLayout饶深,此時實際調用 FrameLayout 的 onLayout():

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // 遍歷子View完成擺放
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

開始遍歷執(zhí)行所有子 View 的 layout 過程如下:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    // 獲取View數(shù)量
    final int count = getChildCount();

    // 獲取父View左側起始點餐曹,就是 - paddingLeft
    final int parentLeft = getPaddingLeftWithForeground();
    // 獲取父View的右側結束點,寬度 - paddRight
    final int parentRight = right - left - getPaddingRightWithForeground();

    // 獲取父View的top點
    final int parentTop = getPaddingTopWithForeground();
    // 獲取父View的bottom結束點
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    // 遍歷擺放所有子View
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 忽略Visibility為GONE的View敌厘,在測量階段它已經(jīng)被忽略掉了
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // 獲取View的測量寬度
            final int width = child.getMeasuredWidth();
            // 獲取View的測量高度
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            // 確定Left坐標
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                //水平居中
                case Gravity.CENTER_HORIZONTAL:
                    // (parentRight - parentLeft - width) / 2 找到中間坐標
                    // parentLeft+, 表示確定啟示坐標
                    // 最后根據(jù)View設置的邊距
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            // 確定Top坐標
            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;
            }

            // 確定了ChildView的left,和top
            // left + width 即 right
            // top + height 即使 bottom
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

可以看到在 layout 階段默認也會忽略 View.GONE台猴。實際上 layout 過程就是確定 View 四個點的坐標信息,計算出 left 點坐標后,left + View 測量寬度即 right 點坐標饱狂,同理計算出 top 點坐標后曹步,top + View 測量高度即 bottom 點坐標。

至此 View 繪制流程的測量和布局兩大階段就已經(jīng)分析完了休讳,不過你是否能夠依照前面的分析箭窜,自己實現(xiàn)一個流式布局呢?

另外衍腥,大家是否有思考過 ScollView 里面嵌套 ListView磺樱,ListView 為什么只能顯示第一行的高度?感興趣的朋友可以去分析下它們的測量過程婆咸。

3. performDraw()
private void performDraw() {
    // 屏幕是否已經(jīng)關閉
    if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
        return;
    } else if (mView == null) {
        // DecorView == null
        return;
    }

    // 是否需要全部重繪
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    mFullRedrawNeeded = false;

    // 正在繪制標記
    mIsDrawing = true;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
    try {
        // 調用draw方法
        draw(fullRedrawNeeded);
    } finally {
        // 繪制完成修改標志位
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

執(zhí)行繪制任務 draw 方法如下:

private void draw(boolean fullRedrawNeeded) {
    // 窗口都關聯(lián)有一個 Surface
    // 在 Android 中竹捉,所有的元素都在 Surface 這張畫紙上進行繪制和渲染,
    // 普通 View(例如非 SurfaceView 或 TextureView) 是沒有 Surface 的尚骄,
    // 一般 Activity 包含多個 View 形成 View Hierachy 的樹形結構块差,只有最頂層的 DecorView 才是對 WindowManagerService “可見的”。
    // 而為普通 View 提供 Surface 的正是 ViewRootImpl倔丈。
    Surface surface = mSurface;
    if (!surface.isValid()) {
        // Surface 是否還有效
        return;
    }

    // 跟蹤FPS
    if (DEBUG_FPS) {
        trackFPS();
    }

    // ...  省略

    // View 滑動通知
    if (mAttachInfo.mViewScrollChanged) {
        mAttachInfo.mViewScrollChanged = false;
        // getViewTreeObserver().addOnScrollChangedListener()
        mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
    }

    // ... 省略

    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }
    // 通知View開始繪制
    // getViewTreeObserver().addOnDrawListener();
    mAttachInfo.mTreeObserver.dispatchOnDraw();

    // ... 省略

    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            // 開啟硬件加速繪制執(zhí)行這里憨闰,最終還是執(zhí)行View的draw開始
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
        } else {
            // 最終調用到drawSoftware
            // surface,每個 View 都由某一個窗口管理需五,而每一個窗口都關聯(lián)有一個 Surface
            // mDirty.set(0, 0, mWidth, mHeight); dirty 表示畫紙尺寸鹉动,對于DecorView,left = 0,
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
    }
}

Surface宏邮,前面文章也有多次提到泽示,在 Android 中,Window 是 View 的容器蜜氨,每個窗口都會關聯(lián)一個 Surface械筛,為窗口提供 Surface 的正是 ViewRootImpl。

  • Surface飒炎。每個 View 都由某一個窗口管理埋哟,而每一個窗口都關聯(lián)有一個 Surface。

在 Android 3.0 之前郎汪,或者沒有啟用硬件加速時赤赊,系統(tǒng)都會使用軟件方式來渲染 UI。軟件繪制需要依賴 CPU怒竿,不過 CPU 對于圖形處理并不是那么高效砍鸠,這個過程完全沒有利用到 GPU 的高性能。

所以從 Android 3.0 開始耕驰,Android 開始支持硬件加速爷辱,直到 Android 4.0 時,才默認開啟硬件加速。

雖然硬件加速繪制與軟件繪制整個流程差異非常大饭弓,但是在 View 層繪制邏輯是一樣的双饥,這里僅以軟件繪制流程為例:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                             boolean scalingRequired, Rect dirty) {

    final Canvas canvas;
    try {
        // 繪制區(qū)域矩形
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;

        // Canvas 實際代表某塊繪制區(qū)域在Sruface
        // Canvas 可以簡單理解為 Skia 底層接口的封裝
        canvas = mSurface.lockCanvas(dirty);

        // The dirty rectangle can be modified by Surface.lockCanvas()
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            // 需要繪制的矩形區(qū)域有變化
            attachInfo.mIgnoreDirtyState = true;
        }

        // 設置像素密度
        // mDensity = context.getResources().getDisplayMetrics().densityDpi;
        canvas.setDensity(mDensity);
    } catch (Surface.OutOfResourcesException e) {
        handleOutOfResourcesException(e);
        return false;
    } catch (IllegalArgumentException e) {
        mLayoutRequested = true;    // ask wm for a new surface next time.
        return false;
    }

    try {

        // ... 省略

        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;

            // mView實際類型是DecorView
            // draw調用到View中
            mView.draw(canvas);

            drawAccessibilityFocusedDrawableIfNeeded(canvas);
        } finally {
            if (!attachInfo.mSetIgnoreDirtyState) {
                // Only clear the flag if it was not set during the mView.draw() call
                attachInfo.mIgnoreDirtyState = false;
            }
        }
    } finally {
        try {
            surface.unlockCanvasAndPost(canvas);
        } catch (IllegalArgumentException e) {
            mLayoutRequested = true;    // ask wm for a new surface next time.
            return false;
        }
    }
    return true;
}

注意 Canvas 的獲取,通過 Surface 的 lock 方法獲得一個 Canvas弟断,Canvas 可以簡單理解為 Skia 底層接口的封裝咏花。

Canvas 作為參數(shù),調用 DecorView 的 draw 方法阀趴,實際調用其父類 View 的 draw()昏翰。

關于繪制流程的三個階段在 View 源碼中都提供了默認的規(guī)則 measure()、layout() 和 draw()刘急,只不過 Android 強制將 measure() 聲明為 final棚菊。

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;

    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * 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)
     */

    // Step 1, 如果需要,繪制背景
    int saveCount;

    // 背景不是透明的
    if (!dirtyOpaque) {
        // 繪制View的背景
        // 重復設置背景色叔汁,會導致過度繪制
        // 避免在布局容器重復設置背景
        drawBackground(canvas);
    }

    // 如果可能统求,跳過第2步和第5步(常見情況)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, 繪制View視圖內容
        // 通常情況下自定義 ViewGroup 不會回調onDraw
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, 繪制子視圖
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            // 繪制浮動View視圖
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, 繪制裝飾(前景,滾動條)
        onDrawForeground(canvas);

        // Step 7, 繪制默認的焦點突出顯示
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
}

Google 工程師非常貼心据块,將繪制階段的任務和步驟做了詳細介紹码邻。

  1. 繪制 Vew 背景
  2. 如果需要,保存畫布的圖層以備褪色
  3. 繪制視圖內容
  4. 分發(fā)繪制子視圖
  5. 如果需要另假,畫出漸退的邊緣并恢復圖層
  6. 繪制裝飾(例如像屋,滾動條)

繪制背景,就是繪制通過 setBackground 設置的 Drawable浪谴,關于背景設置开睡,我們要避免重復設置因苹,這會帶來過渡繪制的問題苟耻。比如被完全遮蓋的布局容器是沒有必要為其設置背景的。

通常情況下扶檐,在定義 ViewGroup 時不會回調 onDraw 方法凶杖,這取決于是否設置了背景。

dispatchDraw 方法主要分發(fā)給 childView 進行繪制任務款筑,在自定義 ViewGroup 實現(xiàn)繪制邏輯時一般會重寫 dispatchDraw() 而不是 onDraw()智蝠。


在開發(fā)過程中,大家是否有注意 LinearLayout奈梳、FrameLayout 和 RelativeLayout 的渲染性能更好杈湾?其實三者在 layout、draw 的耗時相差不大攘须,性能差異主要體現(xiàn)在 measure 階段漆撞。LinearLayout 只會測量一次,水平或垂直方向,但需要注意 weight 的問題浮驳;FrameLayout 如果使用正確也會測量一次悍汛;而 RelativeLayout 要測量多次來確定水平和垂直方向的關聯(lián)關系,但在扁平化布局更具有優(yōu)勢至会,這就需要在根據(jù)業(yè)務場景選擇更優(yōu)的布局容器离咐。

  • ps:現(xiàn)在 Google 更加推薦使用 ConstraintLayout,感興趣的朋友可以深入了解下奉件。

Android 的整個 UI 渲染框架的設計是非常龐大和復雜的宵蛀,經(jīng)過三篇文章介紹 View 繪制流程其實也僅僅是涉及皮毛而已,如果需要更深入了解這塊內容县貌,還需要不斷地學習和查閱相關資料糖埋。

UI 渲染這一塊也是 Google 長期以來非常重視的,基本每次 Google I/O 都會花很多篇幅講這一塊窃这。為了彌補跟 iOS 的差距瞳别,在每個版本都做了大量的優(yōu)化。在后面文章也會聊一聊 Android 渲染的演進杭攻,一起來看下 Google 工程師都做了哪些努力祟敛!


至此 View 繪制流程就已經(jīng)分析完了,正如文中所講這只不過是整個渲染框架的皮毛而已兆解,感興趣的朋友可以繼續(xù)深入研究學習馆铁。文中如有不妥或有更好的分析結果,歡迎您的指出锅睛。

文章如果對你有幫助埠巨,請留個贊吧!

推薦閱讀

其他系列專題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載辣垒,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末印蔬,一起剝皮案震驚了整個濱河市勋桶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侥猬,老刑警劉巖例驹,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異退唠,居然都是意外死亡鹃锈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門瞧预,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屎债,“玉大人寨蹋,你說我怎么就攤上這事∪用” “怎么了已旧?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長召娜。 經(jīng)常有香客問我运褪,道長,這世上最難降的妖魔是什么玖瘸? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任秸讹,我火速辦了婚禮,結果婚禮上雅倒,老公的妹妹穿的比我還像新娘璃诀。我一直安慰自己,他們只是感情好蔑匣,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布劣欢。 她就那樣靜靜地躺著,像睡著了一般裁良。 火紅的嫁衣襯著肌膚如雪凿将。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天价脾,我揣著相機與錄音牧抵,去河邊找鬼。 笑死侨把,一個胖子當著我的面吹牛犀变,可吹牛的內容都是我干的。 我是一名探鬼主播秋柄,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼获枝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了华匾?” 一聲冷哼從身側響起映琳,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜘拉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體有鹿,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡旭旭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了葱跋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片持寄。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡源梭,死狀恐怖,靈堂內的尸體忽然破棺而出稍味,到底是詐尸還是另有隱情废麻,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布模庐,位于F島的核電站烛愧,受9級特大地震影響,放射性物質發(fā)生泄漏掂碱。R本人自食惡果不足惜怜姿,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疼燥。 院中可真熱鬧沧卢,春花似錦、人聲如沸醉者。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撬即。三九已至熟空,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搞莺,已是汗流浹背息罗。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留才沧,地道東北人迈喉。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像温圆,于是被迫代替她去往敵國和親挨摸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內容