深入浅出Android系列之从ViewToBitmap延伸到View的绘制全过程

前言:

最近遇到的一个 Bug 当我们应用将 View 生成图片后 布局会出现布局错乱的现象, 检查代码发现 之前的工程师的 ui和代码逻辑是没什么问题的 那问题应该就是出在生成图片的操作上面,先来看看代码

view.measure(
    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)

(生成图片的代码)

整个代码的逻辑很简单,执行了一遍 view measure() layout () draw() 然后在 draw 中放入我们自己的画布 最后生成 bitmap

经过测试 问题就出现在measure() layout() 先后执行这两个方法 导致布局的宽度和高度出现问题 简单来说 原来一个 TextView 的宽度是 match_conten 当我们调用完这两个方法后就变成了 TextView 文字的宽度 而这个时候如果有其他布局依赖于这个 TextView 的位置就会导致布局出现问题

这个时候就在想 这三个方法其实就是 View 的绘制过程啊 但是在我们执行之前 View 生成时应该是已经执行过一次的 所以我把 measure() layout () 注释掉 单纯执行 draw () 发现代码是可以正常执行的 同时布局没有出现问题

我又试了注释一个 measure() 执行另外两个方法 发现也是正常运行 注释 layout () 执行另外两个方法 发现执行也是正常的 那么为什么先后执行 measure() layout () 布局就会有问题呢

我开始思考View的绘制过程


DecorView 被加载到 Window 开始

我们都知道, PhoneWindow Android 系统中最基本的窗口系统,每个 Activity 会创建一个。同时, PhoneWindow 也是 Activity View 系统交互的接口。 DecorView 本质上是一个 FrameLayout ,是 Activity 中所有 View 的祖先。

Activity startActivity 开始,最终调用到 ActivityThread handleLaunchActivity 方法来创建 Activity ,相关核心代码如下:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
 
    ....
    // 创建Activity,会调用Activity的onCreate方法
    // 从而完成DecorView的创建
    Activity a = performLaunchActivity(r, customIntent);
    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;
        handleResumeActivity(r.tolen, false, r.isForward, !r.activity..mFinished && !r.startsNotResumed);
    }
}
 
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;
    // 调用Activity的onResume方法
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        ...
        if (r.window == null &&& !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            // 得到DecorView
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            // 得到了WindowManager,WindowManager是一个接口
            // 并且继承了接口ViewManager
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                // WindowManager的实现类是WindowManagerImpl,
                // 所以实际调用的是WindowManagerImpl的addView方法
                wm.addView(decor, l);
            }
        }
    }
}


在这里面出现了两个概念 ViewRoot DecorView, 应该简单的提两句 因为 View 的三大流程均是通过 ViewRoot 来完成的 ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager DecorView 的纽带。 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。


View 绘制的流程

Android View 的绘制主要是 3 大流程 分别是 measure layout draw 分别对应着测量 位置 和绘制

同时,在Android 中,主要有两种视图: View ViewGroup View 就是一个独立的视图, ViewGroup 一个容器组件,该容器可容纳多个子视图,即ViewGroup 可容纳多个 View ViewGroup ,且支持嵌套。 虽然ViewGroup 继承于View ,但是在 View 绘制三大流程中,View ViewGroup 它们之间的操作并不完全相同,比如:

  • View ViewGroup 都需要进行 measure ,确定各自的测量宽 / 高。 View 只需直接测量自身即可,而 ViewGroup 通常都必须先测量所有子 View ,最后才能测量自己

  • 通常ViewGroup 先定位自己的位置( layout ),然后再定位其子 View 位置( onLayout

  • View 需要进行 draw 过程,而 ViewGroup 通常不需要(当然也可以进行绘制),因为 ViewGroup 更多作为容器存在,起存储放置功能


measure:

对于 View 的测量主要分为两个步骤

  • 求取 View 的测量规格 MeasureSpec

  • 依据上一步求得的MeasureSpec ,对 View 进行测量,求取得到 View 的最终测量宽 / 高。

那什么是 MeasureSpec呢?MeasureSpec 表示的是一个32 位的整形值,它的高2 位表示测量模式SpecMode ,低30 位表示某种测量模式下的规格大小SpecSize

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0X3 << MODE_SHIFT;
 
    // 不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要
    // 的任何尺寸,通常用于系统内部,应用开发中很少用到。
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
 
    // 精确测量模式,视图宽高指定为match_parent或具体数值时生效,
    // 表示父视图已经决定了子视图的精确大小,这种模式下View的测量
    // 值就是SpecSize的值。
    public static final int EXACTLY = 1 << MODE_SHIFT;
 
    // 最大值测量模式,当视图的宽高指定为wrap_content时生效,此时
    // 子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。
    public static final int AT_MOST = 2 << MODE_SHIFT;
 
    // 根据指定的大小和模式创建一个MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
 
    // 微调某个MeasureSpec的大小
    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return make MeasureSpec(0, UNSPECIFIED);
        }
        int size = getSize(measureSpec) + delta;
        if (size < 0) {
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }
}

MeasureSpec核心代码)

一个MeasureSpec 表达的是:该 View 在该种测量模式( SpecMode )下对应的测量尺寸( SpecSize )。其中, SpecMode 有三种类型:

  • UNSPECIFIED :表示父容器对子 View 未施加任何限制,子 View 尺寸想多大就多大。

  • EXACTLY :如果子 View 的模式为 EXACTLY ,则表示子 View 已设置了确切的测量尺寸,或者父容器已检测出子 View 所需要的确切大小。 这种模式对应于LayoutParams.MATCH_PARENT 和子View 设置具体数值两种情况。

  • AT_MOST :表示自适应内容,在该种模式下, View 的最大尺寸不能超过父容器的 SpecSize ,因此也称这种模式为 最大值模式。 这种模式对应于LayoutParams.WRAP_CONTENT


Measure 的基本流程

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    // ViewGroup没有定义测量的具体过程,因为ViewGroup是一个
    // 抽象类,其测量过程的onMeasure方法需要各个子类去实现
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}
 
// 不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // setMeasureDimension方法用于设置View的测量宽高
    setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
 
// 如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得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:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = sepcSize;
            break;
    }
    return result;
}

(View . measure()源码

View.measure(int, int) 中参数 widthMeasureSpec heightMeasureSpec 是由父容器传递进来的,具体的测量过程请参考后文内容。

需要注意的是,View.measure(int, int) 是一个 final 方法,因此其不可被覆写,实际真正测量 View 自身使用的是 View.onMeasure(int, int) 方法 .

onMeasure 中主要做了三件事

第一件事 通过getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 方法获取得到 View 的推荐最小测量宽 /

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

(View.java)

可以很清晰看出,当 View 没有设置背景时,它的宽度就为 mMinWidth mMinWidth 就是 android:minWidth 这个属性对应设置的值(未设置 android:minWidth 时,其值默认为 0 ),当 View 设置了背景时,它的宽度就是 mMinWidth mBackground.getMinimumWidth() 之中的较大值

getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 其实就是用于获取 View 的最小测量宽 / 高,其具体逻辑为:当 View 没有设置背景时,其最小宽 / 高为 android:minWidth/android:mMinHeight 所指定的值,当 View 设置了背景时,其最小测量宽 / 高为 android:minWidth/android:minHeight 与其背景图片宽 / 高的较大值。

简而言之,View 的最小测量宽 / 高为 android:minWidth/android:minHeight 和其背景宽 / 高之间的较大值。


第二件事 通过getDefaultSize() 获取到 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:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

getDefaultSize()源码)

这里的size 是通过 getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 方法获取得到系统建议 View 的最小测量宽 / 高。

参数measureSpec 是经由 View.measure()==>View.onMeasure()==>View.getDefaultSize() 调用链传递进来的,表示的是当前 View MeasureSpec

getDefaultSize() 内部首先会获取 View 的测量模式和测量大小,然后当 View 的测量模式为 UNSPECIFIED 时,也即未限制 View 的大小,因此此时 View 的大小就是其原生大小(也即 android:minWidth 或背景图片大小),当 View 的测量模式为 AT_MOST EXACTLY 时,此时不对这两种模式进行区分,一律将 View 的大小设置为测量大小(即 SpecSize


第三件事 获取到 View 的测量宽 / 高后,通过 setMeasuredDimension() 记录 View 的测量宽 / 高:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    ...
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
 
// 记录测量宽/高
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
 
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

setMeasuredDimension()源码)

无论是对View 的测量还是 ViewGroup 的测量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法负责,然后真正执行 View 测量的是 View onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。

 

具体来说,View 直接在 onMeasure() 中测量并设置自己的最终测量宽 / 高。在默认测量情况下, View 的测量宽 / 高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同决定,当 View 自身的测量模式为 LayoutParams.UNSPECIFIED 时,其测量宽 / 高为 android:minWidth/android:minHeight 和其背景宽 / 高之间的较大值,其余情况皆为自身 MeasureSpec 指定的测量尺寸。

 

而对于ViewGroup 来说,由于布局特性的丰富性,只能自己手动覆写 onMeasure() 方法,实现自定义测量过程,但是总的思想都是先测量 子 View 大小,最终才能确定自己的测量大小。


layout:

当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。 这也是 layout 干的事情

View.layout() 主要就做了两件事:

  • setFrame() :首先通过 View.setFrame() 来确定自己的布局位置

  • onLayout() setFrame() 是用于确定 View 自身的布局位置,而 onLayout() 主要用于确定 子 View 的布局位置

protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    // Invalidate our old position
    invalidate(sizeChanged);
 
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
}

(setFrame()源码 )


draw

View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。 这也就是 draw 干的事情

public void draw(Canvas canvas) {
    ...
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */
 
    // Step 1, draw the background, if needed
    drawBackground(canvas);
 
    // skip step 2 & 5 if possible (common case)
    ...
    // Step 2, save the canvas' layers
    if (drawTop) {
        canvas.saveLayer(left, top, right, top + length, null, flags);
    }
 
    if (drawBottom) {
        canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
    }
 
    if (drawLeft) {
        canvas.saveLayer(left, top, left + length, bottom, null, flags);
    }
 
    if (drawRight) {
        canvas.saveLayer(right - length, top, right, bottom, null, flags);
    }
    ...
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);
 
    // Step 4, draw the children
    dispatchDraw(canvas);
 
    // Step 5, draw the fade effect and restore layers
    ...
    if (drawTop) {
        ...
        canvas.drawRect(left, top, right, top + length, p);
    }
 
    if (drawBottom) {
        ...
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }
 
    if (drawLeft) {
        ...
        canvas.drawRect(left, top, left + length, bottom, p);
    }
 
    if (drawRight) {
        ...
        canvas.drawRect(right - length, top, right, bottom, p);
    }
    ...
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}

(View.draw()源码)

View.draw() 主要做了以下 6 件事:

  • 绘制背景:drawBackground()

  • 如果有必要的话,保存画布图层:Canvas.saveLayer()

  • 绘制自己:onDraw()

  • 绘制子View dispatchDraw()

  • 如果有必要的话,绘制淡化效果并恢复图层:Canvas.drawRect()

  • 绘制装饰:onDrawForeground()


总结:

View 的绘制主要 三大流程

measure :测量流程,主要负责对 View 进行测量,其核心逻辑位于 View.measure() ,真正的测量处理由 View.onMeasure() 负责。默认的测量规则为:如果 View 的布局参数为 LayoutParams.WRAP_CONTENT LayoutParams.MATCH_PARENT ,那么其测量大小为 SpecSize ;如果其布局参数为 LayoutParams.UNSPECIFIED ,那么其测量大小为 android:minWidth/android:minHeight 和其背景之间的较大值。

自定义View 通常覆写 onMeasure() 方法,在其内一般会对 WRAP_CONTENT 预设一个默认值,区分 WARP_CONTENT MATCH_PARENT 效果,最终完成自己的测量宽 / 高。而 ViewGroup onMeasure() 方法中,通常都是先测量子 View ,收集到相应数据后,才能最终测量自己。

 

layout :布局流程,主要完成对 View 的位置放置,其核心逻辑位于 View.layout() ,该方法内部主要通过 View#setFrame() 记录自己的四个顶点坐标(记录与对应成员变量中即可),完成自己的位置放置,最后会回调 View.onLayout() 方法,在其内完成对 子 View 的布局放置。

不同于 measure 流程首先对 子 View 进行测量,最后才测量自己, layout 流程首先是先定位自己的布局位置,然后才处理放置 子 View 的布局位置。

 

draw :绘制流程,就是将 View 绘制到屏幕上,其核心逻辑位于 View#draw() ,主要就是对 背景、自身内容( onDraw() )、子 View dispatchDraw() )、装饰(滚动条、前景等) 进行绘制。

通常自定义View 覆写 onDraw() 方法,完成自己的绘制即可, ViewGroup 一般充当容器使用,因此通常无需覆写 onDraw()

 

Activity 的根视图(即 DecorView )最终是绑定到 ViewRootImpl ,具体是由 ViewRootImpl#setView(...) 进行绑定关联的,后续 View 绘制的三大流程都是均有 ViewRootImpl 负责执行的。

View 的测量流程中,最关键的一步是求取 View MeasureSpec View MeasureSpec 是在其父容器 MeasureSpec 的约束下,结合自己的 LayoutParams 共同测量得到的,具体的测量逻辑由 ViewGroup.getChildMeasureSpec() 负责。

DecorView MeasureSpec 取决于自己的 LayoutParams 和屏幕尺寸,具体的测量逻辑位于 ViewRootImpl.getRootMeasureSpec()


用逻辑过一遍流程:

1 首先,当 Activity 启动时,会触发调用到 ActivityThread#handleResumeActivity() ,其内部会经历一系列过程,生成 DecorView ViewRootImpl 等实例,最后通过 ViewRootImpl#setView(decor,MATCH_PARENT) 设置 Activity View

ViewRootImpl.setView() 内容通过将其成员属性 ViewRootImpl.mView 指向 DecorView ,完成两者之间的关联。)

 

2 ViewRootImpl 成功关联 DecorView 后,其内部会设置同步屏障并发送一个 CALLBACK_TRAVERSAL 异步渲染消息,在下一次 VSYNC 信号到来时, CALLBACK_TRAVERSAL 就会得到响应,从而最终触发执行 ViewRootImpl.performTraversals() ,真正开始执行 View 绘制流程。

 

3 ViewRootImpl.performTraversals() 内部会依次调用 ViewRootImpl#performMeasure() ViewRootImpl#performLayout() ViewRootImpl#performDraw() 三大绘制流程,其中:

 

4 performMeasure() :内部主要就是对 DecorView 执行测量流程: DecorView#measure() DecorView 是一个 FrameLayout ,其布局特性是层叠布局,所占的空间就是其 子 View 占比最大的宽 / 高,因此其测量逻辑( onMeasure() )是先对所有 子 View 进行测量,具体是通过 ViewGroup.measureChildWithMargins(...) 方法对 子 View 进行测量,子 View 测量完成后,记录最大的宽 / 高,设置为自己的测量大小(通过 View#setMeasuredDimension() ),如此便完成了 DecorView 的测量流程。

 

5 performLayout() :内部其实就是调用 DecorView.layout() ,如此便完成了 DecorView 的布局位置,最后会回调 DecorView.onLayout() ,负责 子 View 的布局放置,核心逻辑就是计算出各个 子 View 的坐标位置,最后通过 child.layout() 完成 子 View 布局。

 

6、 performDraw() :内部最终调用到的是 DecorView.draw() ,该方法内部并未对绘制流程做任何修改,因此最终执行的是 View.draw() ,所以主要就是依次完成对 DecorView 的 背景、子 View dispatchDraw() ) 和 视图装饰(滚动条、前景等) 的绘制


回到开头 Bug 其原因就是在重新调用 measure 后对布局的位置进行重新测量 ,因为子View已经有了真实的宽度,所以子View的宽度就沿用了真实宽度,又 调用了 layout 重新进行布局导致后续有依赖到这个子View的布局都出现了问题

解决方案有 2

1、 只调用 draw () 方法将当前布局绘制到画布上即可

2、 使用 view .drawToBitmap() 方法也可以直接生成 view 的图片




请使用浏览器的分享功能分享到微信等