Measure、Layout和Draw过程分析

本文详细分析了Android应用程序窗口的测量、布局和绘制过程。从DecorView的measure和layout方法开始,深入探讨了ViewGroup如何测量和布局其子视图,包括FrameLayout的onMeasure和measureChildWithMargins方法。最后,讲解了从ViewRoot的draw方法到SurfaceFlinger服务渲染图形缓冲区的完整流程,揭示了Android UI渲染的三层步骤:测量、布局和绘制。
摘要由CSDN通过智能技术生成

Android应用程序窗口的绘图表面在创建完成之后,我们就可以从上到下地绘制它里面的各个视图了,即各个UI元素了。不过在绘制这些UI元素之前,我们还需要从上到下地测量它们实际所需要的大小,以及对它们的位置进行合适的安排,即对它们进行合适的布局。

Android应用程序窗口请求SurfaceFlinger服务创建了一个绘图表面之后,就可以接着请求为该绘图表面创建图形缓冲区,而当Android应用程序窗口往这些图形缓冲区填充好UI数据之后,就可以请求SurfaceFlinger服务将它们渲染到硬件帧缓冲区中去,这样我们就可以看到应用程序窗口的UI了。

Android应用程序窗口一般不会直接去操作分配给它的图形缓冲区,而是通过一些图形库API来操作。对于使用Java来开发的Android应用程序来说,它们一般是使用Skia图形库提供的API来绘制UI的。在Skia图库中,所有的UI都是绘制在画布(Canvas)上的,因此,Android应用程序窗口需要将它的图形缓冲区封装在一块画布里面,然后才可以使用Skia库提供的API来绘制UI。一个Android应用程序窗口里面包含了很多UI元素,这些UI元素是以树形结构来组织的,即它们存在着父子关系,其中,子UI元素位于父UI元素里面,因此,在绘制一个Android应用程序窗口的UI之前,我们首先要确定它里面的各个子UI元素在父UI元素里面的大小以及位置。确定各个子UI元素在父UI元素里面的大小以及位置的过程又称为测量过程和布局过程。因此,Android应用程序窗口的UI渲染过程可以分为测量、布局和绘制三个阶段,如图所示:
在这里插入图片描述
Android应用程序窗口的顶层视图是一个类型为DecorView的UI元素,这个顶层视图最终是由ViewRoot类的成员函数performTraversals来启动测量、布局和绘制操作的,这三个操作分别由DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw来实现的。

接下来,我们就分别从DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw开始,分析Android应用程序窗口的测量、布局和绘制过程。


一、Android应用程序窗口的测量过程

DecorView类的成员函数measure是从父类View继承下来的,因此,我们就从View类的成员函数measure开始分析应用程序窗口的测量过程:

在这里插入图片描述

Step 1. View.measure

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
     
    ......  
  
    int mPrivateFlags;  
    ......  
  
    int mOldWidthMeasureSpec = Integer.MIN_VALUE;  
    ......  
  
    int mOldHeightMeasureSpec = Integer.MIN_VALUE;  
    ......  
  
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
                widthMeasureSpec != mOldWidthMeasureSpec ||  
                heightMeasureSpec != mOldHeightMeasureSpec) {
     
  
            // first clears the measured dimension flag  
            mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
  
            ......  
  
            // measure ourselves, this should set the measured dimension flag back  
            onMeasure(widthMeasureSpec, heightMeasureSpec);  
  
            // flag not set, setMeasuredDimension() was not invoked, we raise  
            // an exception to warn the developer  
            if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
     
                throw new IllegalStateException("onMeasure() did not set the"  
                        + " measured dimension by calling"  
                        + " setMeasuredDimension()");  
            }  
  
            mPrivateFlags |= LAYOUT_REQUIRED;  
        }  
  
        mOldWidthMeasureSpec = widthMeasureSpec;  
        mOldHeightMeasureSpec = heightMeasureSpec;  
    }  
  
    ......  
}  

参数widthMeasureSpec和heightMeasureSpec用来描述当前正在处理的视图可以获得的最大宽度和高度。对于应用程序窗口的顶层视图来说,我们也可以认为这两个参数是用来描述应用程序窗口的宽度和高度。

ViewRoot类的成员变量mPrivateFlags的类型为int,如果它的某一个位的值不等于0,那么就隐含着当前视图有一个相应的操作在等待执行中。ViewRoot类的另外两个成员变量mOldWidthMeasureSpec和mOldHeightMeasureSpec用来保存当前视图上一次可以获得的最大宽度和高度。

当ViewRoot类的成员变量mPrivateFlags的FORCE_LAYOUT位不等于0时,就表示当前视图正在请求执行一次布局操作,这时候函数就需要重新测量当前视图的宽度和高度。此外,当参数widthMeasureSpec和heightMeasureSpec的值不等于ViewRoot类的成员变量mldWidthMeasureSpec和mOldHeightMeasureSpec的值时,就表示当前视图上一次可以获得的最大宽度和高度已经失效了,这时候函数也需要重新测量当前视图的宽度和高度。

当View类的成员函数measure决定要重新测量当前视图的宽度和高度之后,它就会首先将成员变量mPrivateFlags的MEASURED_DIMENSION_SET位设置为0,接着再调用另外一个成员函数onMeasure来真正执行测量宽度和高度的操作。View类的成员函数onMeasure执行完成之后,需要再调用另外一个成员函数setMeasuredDimension来将测量好的宽度和高度设置到View类的成员变量mMeasuredWidth和mMeasuredHeight中,并且将成员变量mPrivateFlags的EASURED_DIMENSION_SET位设置为1。这个操作是强制的,因为当前视图最终就是通过View类的成员变量mMeasuredWidth和mMeasuredHeight来获得它的宽度和高度的。为了保证这个操作是强制的,View类的成员函数measure再接下来就会检查成员变量mPrivateFlags的EASURED_DIMENSION_SET位是否被设置为1了。如果不是的话,那么就会抛出一个类型为IllegalStateException的异常来。

View类的成员函数measure最后就会把参数widthMeasureSpec和heightMeasureSpec的值保存在成员变量mOldWidthMeasureSpec和mOldHeightMeasureSpec中,以便可以记录当前视图上一次可以获得的最大宽度和高度。

View类的成员函数onMeasure一般是由其子类来重写的。例如,对于用来应用程序窗口的顶层视图的DecorView类来说,它是通过父类FrameLayout来重写祖父类View的成员函数onMeasure的。因此,接下来我们就分析FrameLayout类的成员函数onMeasure的实现。


Step 2. FrameLayout.onMeasure

public class FrameLayout extends ViewGroup {
     
    ......  
  
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     
        final int count = getChildCount();  
  
        int maxHeight = 0;  
        int maxWidth = 0;  
  
        // Find rightmost and bottommost child  
        for (int i = 0; i < count; i++) {
     
            final View child = getChildAt(i);  
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
     
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);  
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());  
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());  
            }  
        }  
  
        // Account for padding too  
        maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;  
        maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;  
  
        // Check against our minimum height and width  
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());  
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());  
  
        // Check against our foreground's minimum height and width  
        final Drawable drawable = getForeground();  
        if (drawable != null) {
     
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());  
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());  
        }  
  
        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),  
                resolveSize(maxHeight, heightMeasureSpec));  
    }  
  
    ......  
}  

FrameLayout类是从ViewGroup类继承下来的,后者用来描述一个视图容器,它有一个类型为View的数组mChildren,里面保存的就是它的各个子视图。

FrameLayout类的成员函数onMeasure首先是调用另一个成员函数measureChildWithMargins来测量每一个子视图的宽度和高度,并且找到这些子视图的最大宽度和高度值,保存在变量maxWidth和maxHeight 中。

FrameLayout类的成员函数onMeasure接着再将前面得到的宽度maxWidth和高度maxHeight分别加上当前视图所设置的Padding值,其中,(mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom )表示当前视图的内容区域的左右上下四条边分别到当前视图的左右上下四条边的距离,它们是父类View的四个成员变量,(mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示当前视图的各个子视图所围成的区域的左右上下四条边到当前视视的前景区域的左右上下四条边的距离。从这里就可以看出,当前视图的内容区域的大小就等于前景区域的大小,而前景区域的大小大于等于各个子视图的所围成的区域,这是因为前景区域本来就是用来覆盖各个子视图所围成的区域的。

加上各个Padding值之后,得到的宽度maxWidth和高度maxHeight还不是最终的宽度和高度,还需要考虑以下两个因素:

  1. 当前视图是否设置有最小宽度和高度。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
  2. 当前视图是否设置有前景图。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。

经过上述两步检查之后,FrameLayout类的成员函数onMeasure就得到了当前视图的宽度maxWidth和高度maxHeight。由于得到的宽度和高度又必须要限制在参数widthMeasureSpec和heightMeasureSpec所描述的宽度和高度规范之内,因此,FrameLayout类的成员函数onMeasure就会调用从View类继承下来的成员函数resolveSize来获得正确的大小。得到了当前视图的正确大小之后,FrameLayout类的成员函数onMeasure就可以调用从父类View继承下来的成员函数setMeasuredDimension来将它们为当前视图的大小了。

为了理解参数widthMeasureSpec和heightMeasureSpec的含义,我们继续分析View类的成员函数resolveSize的实现,如下所示:

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
     
    ......  
  
    public static int resolveSize(int size, int measureSpec) {
     
        int result = size;  
        int specMode = MeasureSpec.getMode(measureSpec);  
        int specSize =  MeasureSpec.getSize(measureSpec);  
        switch (specMode) {
     
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  
        case MeasureSpec.AT_MOST:  
            result = Math.min(size, specSize);  
            break;  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
        }  
        return result;  
    }  
  
    ......  
}  

参数measureSpec的值其实是由两部分内容来组成的,最高2位表示一个测量规范,而低30位表示一个宽度值或者高度值。测量规范有三种,分别是0、1和2,使用常量MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY和MeasureSpec.AT_MOST来表示。

当参数measureSpec描述的规范是MeasureSpec.UNSPECIFIED时,就表示当前视图没有指定它的大小测量模式,这时候就使用参数size的值;当参数measureSpec描述的规范是MeasureSpec.AT_MOST时,就表示当前视图的大小等于参数size和参数measureSpec所指定的值中的较小值;当参数measureSpec描述的规范是MeasureSpec.EXACTLY时,就表示当前视图的大小等于参数measureSpec中所指定的值。

回到FrameLayout类的成员函数onMeasure中,我们再来看一下View类的成员函数setMeasuredDimension是如何设置当前视图的大小的,如下所示:

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
     
    ......  
  
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
     
        mMeasuredWidth = measuredWidth;  
        mMeasuredHeight = measuredHeight;  
  
        mPrivateFlags |= MEASURED_DIMENSION_SET;  
    }  
  
    ......  
}

View类的成员函数setMeasuredDimension首先将参数measuredWidth和measuredHeight的值保存在成员变量mMeasuredWidth和mMeasuredHeight中,用来作为当前视图的宽度和高度,并且将成员变量mPrivateFlags的位MEASURED_DIMENSION_SET设置为1,这样返回到前面的Step 1时,就不会抛出一个类型为IllegalStateException的异常了。

FrameLayout类的另一个成员函数measureChildWithMargins是从父类ViewGroup继承下来的,接下来我们就继续分析它的实现,以便可以了解一个视图容器的各个子视图的大小的测量过程。


Step 3. ViewGroup.measureChildWithMargins

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
     
    ......  
  
    protected void measureChildWithMargins(View child,  
            int parentWidthMeasureSpec, int widthUsed,  
            int parentHeightMeasureSpec, int heightUsed) {
     
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin  
                        + widthUsed, lp.width);  
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  
                        + heightUsed, lp.height);  
  
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
    }  
  
    ......  
}  

参数child用来描述当前要测量大小的子视图,参数parentWidthMeasureSpec和parentHeightMeasureSpec用来描述当前子视图可以获得的最大宽度和高度,参数widthUsed和heightUsed用来描述父窗口已经使用了的宽度和高度。ViewGroup类的成员函数measureChildWithMargins必须要综合考虑上述参数,以及当前正在测量的子视图child所设置的大小和Margin值,还有当前视图容器所设置的Padding值,来得到当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec,这是通过调用ViewGroup类的另外一个成员函数getChildMeasureSpec来实现的。

得到了当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,就可以调用它的成员函数measure来设置它的大小了,即执行前面Step 1的操作。注意,如果当前正在测量的子视图child描述的也是一个视图容器,那么它又会重复执行Step 2和Step 3的操作,直到它的所有子孙视图的大小都测量完成为止。

至此,我们就分析完成Android应用程序窗口的测量过程了,接下来我们继续分析Android应用程序窗口的布局过程。


二、Android应用程序窗口的布局过程

DecorView类的成员函数layout是从父类View继承下来的,因此,我们就从View类的成员函数layout开始分析应用程序窗口的布局过程:

在这里插入图片描述

Step 1. View.layout

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
     
    ......  
   
    int mPrivateFlags;  
    ......  
  
    public final void layout(int l, int t, int r, int b) {
     
        boolean changed = setFrame(l, t, r, b);  
        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
     
            ......  
  
            onLayout(changed, l, t, r, b);  
            mPrivateFlags &= ~LAYOUT_REQUIRED;  
        }  
        mPrivateFlags &= ~FORCE_LAYOUT;  
    }  
  
    ......  
} 

参数l、t、r和b分别用来描述当前视图的左上右下四条边与其父视图的左上右下四条边的距离,这样当前视图通过这四个参数就可以知道它在父视图中的位置以及大小。

View类的成员函数layout首先调用另外一个成员函数setFrame来设置当前视图的位置以及大小。设置完成之后,如果当前视图的大小或者位置与上次相比发生了变化,那么View类的成员函数setFrame的返回值changed就会等于true。在这种情况下, View类的成员函数layout就会继续调用另外一个成员函数onLayout重新布局当前视图的子视图。

此外,如果此时View类的成员变量mPrivateFlags的LAYOUT_REQUIRED位不等于0,那么也表示当前视图需要重新布局它的子视图,因此,这时候View类的成员函数layout也会调用另外一个成员函数onLayout。

当前视图的子视图都重新布局完成之后,View类的成员函数layout就可以将成员变量mPrivateFlags的LAYOUT_REQUIRED位设置为0了,因为此时当前视图及其子视图都已经执行了一次布局操作了。

View类的成员函数layout最后还会将成员变量mPrivateFlags的FORCE_LAYOUT位设置为0,也是因为此时当前视图及其子视图的布局已经是最新的了。


Step 2. View.setFrame

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
     
    ......  
   
    int mPrivateFlags;  
    ......  
  
    int mViewFlags;  
    ......  
  
    protected int mLeft;  
    ......  
  
    protected int mRight;  
    ......  
  
    protected int mTop;  
    ......  
  
    protected int mBottom;  
    ......  
  
    private boolean mBackgroundSizeChanged;  
    ......  
  
    protected boolean setFrame(int left, int top, int right, int bottom) {
     
        boolean changed = false;  
  
        ......  
  
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
     
            changed = true;  
  
            // Remember our drawn bit  
            int drawn = mPrivateFlags & DRAWN;  
  
            // Invalidate our old position  
            invalidate();  
  
  
            int oldWidth = mRight - mLeft;  
            int oldHeight = mBottom - mTop;  
  
            mLeft = left;  
            mTop = top;  
            mRight = right;  
            mBottom = bottom;  
  
            mPrivateFlags |= HAS_BOUNDS;  
  
            int newWidth = right - left;  
            int newHeight = bottom - top;  
  
            if (newWidth != oldWidth || newHeight != oldHeight) {
     
                onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);  
            }  
  
            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) {
     
                // If we are visible, force the DRAWN bit to on so that  
                // this invalidate will go through (at least to our parent).  
                // This is because someone may have invalidated this view  
                // before this call to setFrame came in, therby clearing  
                // the DRAWN bit.  
                mPrivateFlags |= DRAWN;  
                invalidate();  
            }  
  
            // Reset drawn bit to original value (invalidate turns it off)  
            mPrivateFlags |= drawn;  
  
            mBackgroundSizeChanged = true;  
        }  
        return changed;  
    }  
  
    ......  
}  

View类的成员变量mLeft、mRight、mTop和mBottom分别用来描述当前视图的左右上下四条边与其父视图的左右上下四条边的距离,如果它们的值与参数left、right、top和bottom的值不相等,那么就说明当前视图的大小或者位置发生变化了。这时候View类的成员函数setFrame就需要将参数left、right、top和bottom的值分别记录在成员变量mLeft、mRight、mTop和mBottom中。在记录之前,还会执行两个操作:

  1. 将成员变量mPrivateFlags的DRAWN位记录在变量drawn中,并且调用另外一个成员函数invalidate来检查当前视图上次请求的UI绘制操作是否已经执行。如果已经执行了的话,那么就会再请求执行一个UI绘制操作,以便可以在修改当前视图的大小和位置之前,将当前视图在当前位置按照当前大小显示一次。
  2. 计算当前视图上一次的宽度oldWidth和oldHeight,以便接下来可以检查当前视图的大小是否发生了变化。

当前视图距离父视图的边距一旦设置好之后,它就是一个具有边界的视图了,因此,View类的成员函数setFrame接着还会将成员变量mPrivateFlags的HAS_BOUNDS设置为1。

View类的成员函数setFrame再接下来又会计算当前视图新的宽度newWidth和高度newHeight,如果它们与上一次的宽度oldWidth和oldHeight的值不相等,那么就说明当前视图的大小发生了变化,这时候就会调用另外一个成员函数onSizeChanged来让子类有机会处理这个变化事件。

View类的成员函数setFrame接下来继续判断当前视图是否是可见的,即成员变量mViewFlags的VISIBILITY_MASK位的值是否等于VISIBLE。如果是可见的话,那么就需要将成员变量mPrivateFlags的DRAWN位设置为1,以便接下来可以调用另外一个成员函数invalidate来成功地执行一次UI绘制操作,目的是为了将当前视图马上显示出来。

View类的成员变量mPrivateFlags的DRAWN位描述的是当前视图上一次请求的UI绘制操作是否已经执行过了。如果它的值等于1,就表示已经执行过了,否则的话,就表示还没在等待执行。前面第一次调用View类的成员函数invalidate来检查当前视图上次请求的UI绘制操作是否已经执行时,如果发现已经执行了,那么就会重新请求执行一次新的UI绘制操作,这时候会导致当前视图的成员变量mPrivateFlags的DRAWN位重置为0。

注意,新请求执行的UI绘制只是为了在修改当前视图的大小以及大小之前,先将它在上一次设置的大小以及位置中绘制出来,这样就可以使得当前视图的大小以及位置出现平滑的变换。换句话说,新请求执行的UI绘制只是为了获得一个中间效果,它不应该影响当前视图的绘制状态,即不可以修改当前视图的成员变量mPrivateFlags的DRAWN位。因此,我们就需要在前面第一次调用View类的成员函数invalidate前,先将当前视图的成员变量mPrivateFlags的DRAWN位保存下来,即保存在变量drawn中,然后等到调用之后,再将变量drawn的值恢复到当前视图的成员变量mPrivateFlags的DRAWN位中去。

另一方面,如果当前视图的大小和位置发生了变化,View类的成员函数setFrame还会将成员变量mBackgroundSizeChanged的值设置为true,以便可以表示当前视图的背景大小发生了变化。

最后,View类的成员函数setFrame将变量changed的值返回给调用者,以便调用者可以知道当前视图的大小和位置是否发生了变化。

接下来,我们继续分析View类的成员函数invalidate的实现,以便可以了解当前视图是如何执行一次UI绘制操作的。


Step 3. View.invalidate

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
     
    ......  
  
    protected ViewParent mParent;  
    ......  
   
    int mPrivateFlags;  
    ......      
  
    public void invalidate() {
     
        ......  
  
        if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
     
            mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;  
            final ViewParent p = mParent;  
            final AttachInfo ai = mAttachInfo;  
            if (p != null && ai != null) {
     
                final Rect r = ai.mTmpInvalRect;  
                r
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值