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

UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识如何优化 UI 渲染两部分内容。


UI 优化系列专题
  • UI 渲染背景知识

View 绘制流程之 setContentView() 到底做了什么?
View 绘制流程之 DecorView 添加至窗口的过程
深入 Activity 三部曲(3)View 绘制流程
Android 之 LayoutInflater 全面解析
关于渲染,你需要了解什么?
Android 之 Choreographer 详细分析

  • 如何优化 UI 渲染

Android 之如何优化 UI 渲染(上)
Android 之如何优化 UI 渲染(下)


在 View 绘制流程系列,分别介绍了 View 的创建以及添加至窗口的过程,它们也是为今天要分析的 View 绘制任务做的铺垫,View 的绘制流程主要包含三个阶段:measure -> layout -> draw。

在具体分析之前,还是通过几个问题来了解下今天要分析的内容:

  • Handler 异步消息的作用?
  • Android 是如何解决不确定的布局尺寸?即 MATCH_PARENT 或 WRAP_CONTENT。
  • 为什么 View.GONE 不会占用布局空间?
  • getWidth() 和 getMeasuredWidth() 有什么区别?在什么时候调用才会有值?

requestLayout()

View 绘制的起始点是在 ViewRootImpl 的 requestLayout 方法,前面有分析到在该方法首先会检查是否在原线程。这里简单说下,UI 的绘制并非一定要在主线程,但是它要求是在原线程,绝大多数操作系统 UI 框架都是单线程的,这主要是因为多线程的 UI 框架在设计上会非常复杂。

然后通过 scheduleTraversals 方法发送消息开始 View 绘制流程:

 void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //编舞者,可以用它来监听帧频
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //... 省略
    }
}

注意 postSyncBarrier() 发送同步屏障消息,可能很多人不知道 Handler 有两种 Message 类型。

  • 同步消息(普通消息)
  • 异步消息(Android 4.1 新增,配合 VSYNC 信号)

Handler 的构造方法提供了用于区分两种消息的构造方法,不过它们被 @hide 了,但是 Message 为我们敞开了:

//设置为异步消息
public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

//获取当前消息类型,是同步消息还是异步消息
public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}

一般情况下同步消息和异步消息的处理方式并没有什么区别,只有在设置了同步屏障时才会出现差异。同步屏障为 Handler 消息机制增加了一种简单的优先级关系,异步消息的优先级要高于同步消息,用于配合系统的 VSYNC 信号。简单点说,设置了同步屏障之后,Handler 只会处理异步消息。

但是发送同步屏障的接口并没有对应用开发者公开,其实它的主要作用是为了更快的响应 UI 绘制事件,避免长时间等待于消息队列。

继续分析,发送 UI 绘制任务 mTraversalRunnable 到 Choreographer。

//编舞者,可以用它来监听帧频
mChoreographer.postCallback(
   Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
  1. Choreographer 是负责获取 VSYNC 同步信号并统一调度 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!");
        }
        // 为当前线程创建一个Choreographer
        return new Choreographer(looper, VSYNC_SOURCE_APP);
    }
};

看下 Choreographer 的构造方法,注意 FrameHandler 接收对应线程的 Looper 对象。

private Choreographer(Looper looper) {
    //当前线程Looper
    mLooper = looper;
    //创建handle对象,用于处理消息
    mHandler = new FrameHandler(looper);
    //创建VSYNC的信号接受对象
    mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
    //初始化上一次frame渲染的时间点
    mLastFrameTimeNanos = Long.MIN_VALUE;
    //计算帧率,也就是一帧所需的渲染时间,getRefreshRate是刷新率,一般是60
    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
    //创建消息处理队列
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}
  1. mTraversalRunnable

mTraversalRunnable 本质是一个 Runnable,通过 mChoreographer.postCallback() 发送到主线程消息队列(这里以主线程绘制流程做分析)。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        //开始执行绘制遍历
        doTraversal();
    }
}

doTraversal() 真正开始执行 UI 绘制的遍历过程:

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

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //执行UI绘制的遍历过程
        performTraversals();

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

mTraversalScheduled 变量主要防止重复的绘制任务,removeSyncBarrier 方法移除同步屏障,因为此时 View 绘制任务已经处于执行过程中。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);
                // 执行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 系统为自适应布局尺寸引入了 LayoutParams.MATCH_PARENT 和 LayoutParams.WRAP_CONTENT,这样就会有不确定的情况,那 Android 又是如何解决不确定的布局尺寸呢?

答案就是 MeasureSpec( 测量规格),它本质是 4 个字节的 int 数值,主要包含两部分:高 2 位表示测量模式,低 30 位表示测量大小

  1. 测量模式
  • MeasureSpec.EXACTLY:精确大小,父容器已经测量出所需要的精确大小,这也是我们 childView 的最终大小 — MATCH_PARENT。

  • MeasureSpec.AT_MOST: 最终的大小不能超过我们的父容器 — WRAP_CONTENT。

  • UNSPECIFIED:不确定的,源码内部使用,一般在 ScorllView、ListView 中能看到这些,需要动态测量。

在 MeasureSpec 中测量模式关键方法:

    /**
     * 获取测量模式,取最高两位
     */
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        //MODE_MASK为110000000000000000000000000000
        return (measureSpec & MODE_MASK);
    }
  1. 测量大小
    测量大小是根据测量模式来确定,在 Measure 流程中,系统将 View 的 LayoutParams 根据父容器施加的规则转化成对应的 MeasureSpec,在 onMeasure() 中根据这个 MeasureSpec 来确定 View 的测量宽高。

在 MeasureSpec 中测量大小关键方法:

 /**
  * 获取测量大小,取低30位
  */
 public static int getSize(int measureSpec) {  
      //~MODE_MASK为00111111111111111111111111111111
       return (measureSpec & ~MODE_MASK);
 }

说道这里,需要先看下表示窗口视图 DecorView 的测量模式和测量大小:

// 获取DecorView的宽高测量规格
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;
    }
    //返回测量规格
    return measureSpec;
}

由于 DecorView 的宽高为 MATCH_PARENT,故它的宽高测量规格都为:EXACTLY + windowSize(窗口大小,视具体手机屏幕决定)。

1. performMeasure()

接下来开始 View 的测量工作,注意 mView 实际是 DecorView 如下:

 /**
 * 执行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
    // 将宽度测量规格和高度测量规格整合成long,高32位为宽度,低32位为高度测量规格
    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;

    // 这里主要是做优化,防止在未发生变化的情况下,无谓的测量工作
    // 宽度和高度测量规格是否发生过变化
    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);

    // 如果与上次测量结果发生变化此时需要重写测量
    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 表示当前测量已经失效(缓存不存在),需要重新测量
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            // 否则测量结果未发生变化,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;
    }

    // 保存最新测量规格
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    // 缓存当前测量结果
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

可以看到系统对 View 的测量工作做了大量的优化,只为有效减少无谓的测量工作,提高 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 自行实现,所以在 ViewGroup 中没有实现该方法。

我们先来看下 View 的默认测量过程 onMeasure 方法如下:

/**
  *  View默认的onMeasure方法
  */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //getSuggestedMinimumWidth确定当前View的最小尺寸,根据最小尺寸与背景尺寸取较大值
    //getDefaultSize()确定子View的尺寸
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
  • getSuggestedMinimumXxx(),从名字看是获取 View 最小建议尺寸,以宽度为例:
/**
 * 确定View的最小宽度,根据最小宽度和背景宽度取较大值
 */
protected int getSuggestedMinimumWidth() {
    // 没有背景图,则使用最小宽度
    // 否则取最小宽度和背景宽度的较大值
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}   
  • getDefaultSize(),根据最小建议尺寸和测量大小决定 View 的最终尺寸:
/**
 * size为当前View的最小尺寸
 * measureSpec,当前View的测量规格
 */
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() 最终执行到 setMeasuredDimensionRaw() 设置 View 的测量大小:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    // 赋值给View成员
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    // 标志,已经设置View的测量大小
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

记得上面说到的自定义 View ,如果在其 onMeasure 方法中没有调用 setMeasureDimension 方法,将会抛出异常,此时在方法最后修改该标志位:

// 标志,已经设置View的测量大小
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;

注意,文章开头提出的问题 getMeasuredWidth() / getMeasuredHeight() 在什么时候才会获取到值?答案就在这里,setMeasuredDimensionRaw 方法执行结束,此时调用 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 的默认测量规则,但由于 DecorView 继承自 FrameLayout,所以此时 onMeasure 实际调用的是 FrameLayout 的 onMeasure 方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 获取子View数量
    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,获取其他布局参数
            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 循环开始遍历测量所有子 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的宽度测量规格,根据父容器施加的规则,加上宽度内边距
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    // 子View的高度测量规格,根据父容器施加的规则,加上高度内边距
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

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

看下如何确定子 View 的测量规格,getChildMeasureSpec 方法如下:

/**
 * 根据父容器的测量规格确定子View的测量规格
 */
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需要动态的测量
                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需要动态的测量
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    //noinspection ResourceType
    //生成子View的测量规格
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

前面也有简单提到子 View 要根据父 View 施加的测量规格决定自己的测量大小。关于子 View 的测量规格这里做下简单总结:

  1. 如果子 View 的 LayoutParams 为具体数值,此时无论父 View 施加测量模式是什么,子 View 的测量规格都为 EXACTLY + childDimension(在 LayoutParams 中设置的具体数值)。

  2. 如果子 View 的 LayoutParams 为 MATCH_PARENT,当父 View 的测量模式为 EXACTLY 时,子 View 的测量规格为 EXACTLY + 小于等于父 View 的测量大小;当父 View 的测量模式为 AT_MOST 时,子 View 的测量规格为 AT_MOST + 小于等于父 View 的测量大小;当父 View 的测量大小为 UNSPECIFIED 时,子 View 的测量规格为 UNSPECIFIED + 0。

  3. 如果子 View 的 LayoutParams 为 WRAP_CONTENT,当父 View 的测量模式为 EXACTLY,子 View 的测量规格为 AT_MOST + 小于等于父 View 测量大小;当父 View 的测量规格为 AT_MOST 时,子 View 的测量规格为 AT_MOST + 小于等于父 View 的测量大小;当父 View 的测量模式为 UNSPECIFIED 时,子 View 的测量规格为 UNSPECIFIED + 0。

在 measureChildWithMargins 方法最后,调用 View 的 measure 方法完成测量结果,关于 View 的默认测量流程前面已经做过分析,感兴趣的朋友可以去分析下例如 ImageView、TextView 的测量过程。

重新回到 FrameLayout 的 onMeasure 方法,注意看在第一个 for 循环,如果当前 FrameLayout 的宽 / 高测量模式存在非 EXACTLY(即 measureMatchParentChildren == true),此时它所包含的子 View 存在 LayoutParams 为 MATCH_PARENT 时,会将该 View 记录在 mMatchParentChilden 中。被记录下的 View 需要二次测量确定大小

小结
  1. 用一张图再来了解下 View 的整个测量过程:
View 树的源码 measure 流程图
  1. 用一张表格总结下子 View 的测量规格:
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 时发生二次测量。
  • 确定大小的 FrameLayout,即就是保证 FrameLayout 的测量模式为 MeasureSpec.EXACTLY。

  • 确定大小的 ChildView,或者使用 WRAP_CONTENT 。

至此 View 的测量过程就分析完了,不过测量过程涉及的细节内容非常多,感兴趣的朋友可以继续深入分析。

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阶段,如果当前需要测量,但是发现已经缓存了该测量结果时,measure阶段
    // 并没有真正执行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;

    // 根据是否有光影效果
    // changed标志View坐标是否发生变化
    // setFrame 保存新的坐标位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 这也是做一层优化,避免无谓的遍历layout过程
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 执行布局摆放,当前实际是 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,表示当前布局坐标发生新的变化
        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坐标是否真的发生变化
    boolean changed = false;

    // 判断View的坐标信息是否发生变化
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        // 表示当前View坐标发生变化
        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大小是否发生变化,
        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方法被回调
            // 注意如果仅是坐标位置发生变化,View自身尺寸未发生变化sizeChange是不会被调用的
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        // ... 省略
    }
    return changed;
}

setFrame 方法就是为 View 赋值新的 left、right、top 和 bottom 四个坐标点,View 的坐标位置真正发生变化, changed 变量才会返回 true。

  • 注意看 sizeChanged 变量,只有当 View 的宽 / 高发生变化才会回调 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方法执行结束之后,回调
          // 此时表示所有的View都已经布局完成,可以获取到任何View组件的宽度、高度、左边、右边等信息
          // 需要注意多次调用带来的影响
       }
 });

performLayout 方法执行完毕,此时所有的 View 已经布局完成,便会回调 onGlobalLayout 通知。

  1. view.post()

相信很多人都使用过该方法,并且知道任务会被添加到主线程(当前分析主线程渲染)消息队列等待执行;但是它背后的执行原理可能大多数开发者并不一定了解,简单来说,它保证了在 View 绘制流程结束后回调相关任务,此时我们就可以正确获取到 View 的宽高了。具体你可以参考《Android 之你真的了解 View.post() 原理吗?

重新回到 DecorView 的 layout 方法,如果需要布局则调用 onLayout 方法,onLayout() 在 View 中默认为空实现,但是在 ViewGroup 将其重写为 abstract,即强制 ViewGroup 的子类重写该方法,因为布局容器必须实现 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 */);
}

开始遍历执行所有子 View 的 layout 过程如下:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    // 获取View数量
    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,在测量阶段它已经被忽略掉了
        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+, 表示确定启示坐标
                    // 最后根据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 绘制流程的测量和布局两大阶段就已经分析完了,不过你是否能够依照前面的分析,自己实现一个流式布局呢?

另外,大家是否有思考过 ScollView 里面嵌套 ListView,ListView 为什么只能显示第一行的高度?感兴趣的朋友可以去分析下它们的测量过程。

3. performDraw()
private void performDraw() {
    // 屏幕是否已经关闭
    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);
    }
}

执行绘制任务 draw 方法如下:

private void draw(boolean fullRedrawNeeded) {
    // 窗口都关联有一个 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()) {
            // 开启硬件加速绘制执行这里,最终还是执行View的draw开始
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
        } else {
            // 最终调用到drawSoftware
            // surface,每个 View 都由某一个窗口管理,而每一个窗口都关联有一个 Surface
            // mDirty.set(0, 0, mWidth, mHeight); dirty 表示画纸尺寸,对于DecorView,left = 0,
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
    }
}

Surface,前面文章也有多次提到,在 Android 中,Window 是 View 的容器,每个窗口都会关联一个 Surface,为窗口提供 Surface 的正是 ViewRootImpl。

  • Surface。每个 View 都由某一个窗口管理,而每一个窗口都关联有一个 Surface。

在 Android 3.0 之前,或者没有启用硬件加速时,系统都会使用软件方式来渲染 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 {
        // 绘制区域矩形
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;

        // Canvas 实际代表某块绘制区域在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) {
            // 需要绘制的矩形区域有变化
            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 作为参数,调用 DecorView 的 draw 方法,实际调用其父类 View 的 draw()。

关于绘制流程的三个阶段在 View 源码中都提供了默认的规则 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. 分发绘制子视图
  5. 如果需要,画出渐退的边缘并恢复图层
  6. 绘制装饰(例如,滚动条)

绘制背景,就是绘制通过 setBackground 设置的 Drawable,关于背景设置,我们要避免重复设置,这会带来过渡绘制的问题。比如被完全遮盖的布局容器是没有必要为其设置背景的。

通常情况下,在定义 ViewGroup 时不会回调 onDraw 方法,这取决于是否设置了背景。

dispatchDraw 方法主要分发给 childView 进行绘制任务,在自定义 ViewGroup 实现绘制逻辑时一般会重写 dispatchDraw() 而不是 onDraw()。


在开发过程中,大家是否有注意 LinearLayout、FrameLayout 和 RelativeLayout 的渲染性能更好?其实三者在 layout、draw 的耗时相差不大,性能差异主要体现在 measure 阶段。LinearLayout 只会测量一次,水平或垂直方向,但需要注意 weight 的问题;FrameLayout 如果使用正确也会测量一次;而 RelativeLayout 要测量多次来确定水平和垂直方向的关联关系,但在扁平化布局更具有优势,这就需要在根据业务场景选择更优的布局容器。

  • ps:现在 Google 更加推荐使用 ConstraintLayout,感兴趣的朋友可以深入了解下。

Android 的整个 UI 渲染框架的设计是非常庞大和复杂的,经过三篇文章介绍 View 绘制流程其实也仅仅是涉及皮毛而已,如果需要更深入了解这块内容,还需要不断地学习和查阅相关资料。

UI 渲染这一块也是 Google 长期以来非常重视的,基本每次 Google I/O 都会花很多篇幅讲这一块。为了弥补跟 iOS 的差距,在每个版本都做了大量的优化。在后面文章也会聊一聊 Android 渲染的演进,一起来看下 Google 工程师都做了哪些努力!


至此 View 绘制流程就已经分析完了,正如文中所讲这只不过是整个渲染框架的皮毛而已,感兴趣的朋友可以继续深入研究学习。文中如有不妥或有更好的分析结果,欢迎您的指出。

文章如果对你有帮助,请留个赞吧!

推荐阅读

其他系列专题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容