浅答 View 绘制部分面试知识点

View 的绘制流程

答案参考自:

MeasureSpec 是什么

spec 英[spek]n.规格 vt.按特定标准设计并制造

MeasureSpec参与了Viewmeasure过程。在测量过程中,系统将ViewLayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的测量宽/高。

MeasureSpec代表一个32位的int值,高两位代表 测量模式SpecMode,低30位代表 该测量模式下的规格大小SpecSize

MeasureSpecSpecModeSpecSize之间可以通过位运算互相求出。

每个Viewmeasure过程之前都会先计算其MeasureSpec的值,然后再对其进行measure

注意View的宽和高各有一个MeasureSpec值,分别为widthMeasureSpecheightMeasureSpec

SpecMode的三种状态

  1. UNSPECIFIED

    父容器不对View做任何的限制,要多大给多大。这种情况一般用于系统的内部,表示一种测量方式的状态。

  2. EXACTLY

    父容器已经测出了View所需要的精确大小,此时的大小就是View的最终大小。

    它对应于LayoutParamsmatch_parent和具体的数值这两种模式。

  3. AT_MOST

    父容器指定了一个可用的大小,View的大小不能大于这个值。

    它对应于LayoutParams中的wrap_content

子View创建MeasureSpec创建规则是什么

MeasureSpecLayoutParams的对应关系

对于DecorView和普通View来说,MeasureSpec的转化过程略有不同。

对于DecorView

DecorViewMeasureSpec由窗口的大小和DecorView自身的LayoutParams共同决定的。

通过在ViewRootImpl中的measureHierarchy方法中,调用getRootMeasureSpec方法,获得DecorViewMeasureSpec

其中getRootMeasureSpec方法中,根据自身LayoutParams的不同大小,共有三种情况:

  1. LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
  2. LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  3. 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通View

普通ViewMeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定的。

通过在父容器的measureChildWithMargins中的getChildMeasureSpec方法获得ViewMeasureSpec

其中getChildMeasureSpec方法的参数中使用到了父容器的MeasureSpec。在这个方法中通过switchif语句,对ViewMeasureSpec进行了决定。结果如下:

childLayoutParams \ parentMeasureSpec EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

上图可以简单记为:除了dp/px的精确模式情况外,三种MeasureSpec的级别为:EXACTLY < AT_MOST < UNSPECIFIEDchildMeasureSpec = max(childLayoutParams , parentMeasureSpec)。(实际上并无级别之分

通过上面的表格,可以更加直观的看出:普通ViewMeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定的。

自定义View wrap_content不起作用的原因

对于直接继承View的自定义View来说,需要重写onMeasure方法并设置在wrap_content情况时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent

因为当自定义View处于wrap_content时,它的SpecModeAT_MOST模式,由前文可知,当父容器无论为AT_MOST还是ECACTLY模式时,自定义View都为AT_MOST模式且尺寸大小为parentSize,也就是父容器的剩余空间,即自定义View的大小变得与父容器的剩余空间大小一致,显然不是我们需要的。

解决方法:

解决方法也是非常简单的。我们只需要在onMeasure方法中对

  1. width方向为AT_MOST模式
  2. height方向为AT_MOST模式
  3. width方向以及height方向都为AT_MOST模式
  4. 其他的情况

这四个状态单独判断,分别使用setMeasuredDimension方法直接为自定义View设置我们需要的值即可。

源码中对于TextViewImageView等控件的wrap_content也做了特殊的处理。

在Activity中获取某个View的宽高有几种方法

由于Activity的启动和View的测量过程并不是同步进行的,因此无法保证在Activity启动时某个View已经测量完毕了。所以我们在Activity启动的时候,如果直接通过getMeasuredWidth/getMeasuredHeight方法获得的值可能为0。

以下有四种方法可以解决这个问题(详细代码见《Android开发艺术探索》190页)。

  1. Activity/View#onWindowFocusChanged

    ActivityonResumeonPause方法启动时,onWindowFocusChanged方法也会同时被调用,可以重写里面的内容,使之计算View的宽高。

    注意onWindowFocusChanged方法会在View已经初始化完毕后才开始调用。

  2. view.post(runnable)

    通过post可以将一个runnable投递都消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。

  3. ViewTreeObserver

    ViewTreeObserver中的众多回调可以完成这个功能。

    比如OnGlobalLayoutListener接口,当View树的状态发生改变或者View树内部的View的可见性发生改变的时候,onGlobalLayout方法将被回调,此时就可以获取View的宽高了。

  4. view.measure(int widthMeasureSpec, int heightMeasureSpec)

    可以主动调用该方法开始计算view的宽高。

    1. match_parent

      由于正常的measure过程中,我们需要父容器的MeasureSpec的值,而此时父容器并没开始计算,我们无从得知这两个值的大小。故我们无法对match_parentview计算其宽高。

    2. dp/px

      因为有精确的数值了,所以我们可以直接使用这个精确的数值通过MeasureSpec.makeMeasureSpec方法构建viewMeasureSpec

    3. wrap_content

      直接将MeasureSpec.makeMeasureSpec方法中参数的值设置为(1<<30)-1即可,这是View理论上可以支持的最大值,所以这样构建MeasureSpec是合理的。

onCreateonResume中可以获取View的宽高吗?怎么做?
View#post 为什么可以获取?

答案参考自:

onCreateonResume中可以获取 View 的宽高吗?怎么做?

View 的测绘绘制流程就是从 ViewRootImpl#performTraversals 开始的,而这个方法的调用是在 onResume 方法之后,所以在 onCreate 和 onResume 方法中拿不到 View 的测量值。

View 的宽高是在 onLayout 阶段才能最终确定的,而在 Activity#onCreate 中并不能保证 View 已经执行到了 onLayout 方法,也就是说 Activity 的声明周期与View的绘制流程并不是一一绑定。所以 onCreate 和 onResume 中获取不到 View 的宽高值。以 Handler 为基础,View#post 将传入任务的执行时机调整到 View 绘制完成之后。

View#post 为什么可以获取?

1
2
3
4
5
6
7
view.post(new Runnable() {
@Override
public void run() {
int width = view.getWidth();
int height = view.getHeight();
}
});

这样写一般是在 Activity 的 onResume 方法中,因为 onResume 执行在 View 初始化之前,如果在 onResume 中直接获取 View 宽高是获取不到的。

使用 view#post 就能获取到,因为 view#post 是向主 Handler 的 MessageQueue 中插入一条待执行消息,但是因为系统在 ViewRoot 中初始化 View 时也是利用 Handler 机制,平且为了优先执行 View 的初始化设置了同步屏障,导致 view#post 插入的消息会在 View 初始化之后执行,那么肯定就能获取到 View 的宽高啦!

Runnable的执行时机具体是什么

Android 7.0 之后,view.post()中的runnbale 能确定被执执行。具体来说:

Android 7.0之后,除了performTraversal中会调用外,在View的dispatchAttachedToWindow中也会调用,但Android 7.0之后不管在主线程还是在子线程都可以成功执行view.post内部逻辑,并不是因为增加了调用时机,而是取消了ThreadLocal机制,使得 不管在主线程还是子线程调用view.post方法,都会将runnable对象丢到主线程的任务队列中,更新UI或者获取view的信息

View#postHandler#post的区别

答案参考自:

  1. Handler.post,它的执行时间基本是等同于onCreate里那行代码触达的时间;

  2. View.post,则不同,它说白了执行时间一定是在Activity#onResume发生后才开始算的;或者换句话说它的效果相当于你上面的View.post方法是写在Activity#onResume里面的(但只执行一次,因为onCreate不像onResume会被多次触发);

  3. 当然,虽然这里说的是post方法,但对应的postDelayed方法区别也是类似的。

getWidth()方法和getMeasureWidth()方法的区别

答案参考自:

  1. getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。getMeasuredWidth()获取的是View原始的大小,也就是这个ViewXML文件中配置或者是代码中设置的大小。getWidth()获取的是这个View最终显示的大小,这个大小有可能等于原始的大小,也有可能不相等。只要在代码里重新修改了子控件的摆放位置,getWidth()getMeasureWidth()的值就会不同。

  2. getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

View加载流程(setContentView)

答案参考自:

  1. 通过Activity的setContentView方法间接调用PhoneWindow的setContentView(),在PhoneWindow中通过getLayoutInflate()得到LayoutInflate对象。

  2. 通过LayoutInflate对象去加载View,主要步骤是:

    1. 通过xml的Pull方式去解析xml布局文件,获取xml信息,并保存缓存信息,因为这些数据是静态不变的。
    2. 根据xml的tag标签通过反射创建View逐层构建View。
    3. 递归构建其中的子View,并将子View添加到父ViewGroup中。

其中,有四种加载XML文件的常用方法:

  1. 使用view的静态方法

    1
    View view = View.inflate(context, R.layout.child, null);
  2. 通过系统获取

    1
    2
    LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View view = inflater.inflate(R.layout.child, null);
  3. 通过LayoutInflater

    1
    2
    LayoutInflater inflater = LayoutInflater.from(context);
    View view = inflater.inflate(R.layout.child, null);
  4. 通过getLayoutInflater

    1
    View view = getLayoutInflater().inflate(R.layout.child, null);

View 加载过程结束后,便会开始 View 的绘制流程了。

invalidate()postInvalidate() 的区别

这两个方法都是在重绘当前控件的时候调用的。

invalidate在UI线程中调用,postInvalidate在非UI线程中调用。 因为android的UI线程是非线程安全的,所以在非UI线程中,需要使用postInvalidate来使View重绘。

view调用invalidate将导致当前view的重绘(draw调用),view的父类将不会执行draw方法;viewGroup调用invalidate会使viewGroup的子view调用draw,也就是viewGroup内部的子view进行重绘。

requestLayout()onLayout() 的区别

requestLayout()

requestLayout方法只会**导致当前viewmeasurelayout**,而draw不一定被执行,只有当view的位置发生改变才会执行draw方法,因此如果要使当前view重绘需要调用invalidate

onLayout()

在很多情况下requestLayout是不需要被调用的。

例如,我们把一个AbsoluteLayout里面的childView挪动一下位置。我们仅仅需要调用的可能就是重新布局当前AbsoluteLayout,然后调用invalidate方法进行重绘。而不是从当前View向上的整个View树形结构都要重新layout,onLayout,measure,onMeasure一次。在这种情况下可以直接调用onLayout。然后调用invalidate进行重绘。很明显可以提升绘制效率。

由于父View的layout实现了会通知布局的listener。但是由于无法得到listener,因此调用onlayout的时候无法对其进行通知,这也是这种实现的缺陷。

自定义 View 的流程和注意事项

答案参考自:


TODO

  1. invalidate怎么局部刷新
  2. Android绘制和屏幕刷新机制原理`

浅答 View 绘制部分面试知识点
https://luoyuy.top/posts/00e069ccc001/
作者
LuoYu-Ying
发布于
2022年5月14日
许可协议