浅答 View 绘制部分面试知识点
View 的绘制流程
答案参考自:

MeasureSpec 是什么
spec英[spek]n.规格vt.按特定标准设计并制造
MeasureSpec参与了View的measure过程。在测量过程中,系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的测量宽/高。
MeasureSpec代表一个32位的int值,高两位代表 测量模式SpecMode,低30位代表 该测量模式下的规格大小SpecSize。
MeasureSpec与SpecMode、SpecSize之间可以通过位运算互相求出。
每个View的measure过程之前都会先计算其MeasureSpec的值,然后再对其进行measure。
注意:View的宽和高各有一个MeasureSpec值,分别为widthMeasureSpec和heightMeasureSpec。
SpecMode的三种状态
UNSPECIFIED父容器不对
View做任何的限制,要多大给多大。这种情况一般用于系统的内部,表示一种测量方式的状态。EXACTLY父容器已经测出了
View所需要的精确大小,此时的大小就是View的最终大小。它对应于
LayoutParams的match_parent和具体的数值这两种模式。AT_MOST父容器指定了一个可用的大小,
View的大小不能大于这个值。它对应于
LayoutParams中的wrap_content。
子View创建MeasureSpec创建规则是什么
MeasureSpec和LayoutParams的对应关系
对于DecorView和普通View来说,MeasureSpec的转化过程略有不同。
对于DecorView:
DecorView的MeasureSpec由窗口的大小和DecorView自身的LayoutParams共同决定的。
通过在ViewRootImpl中的measureHierarchy方法中,调用getRootMeasureSpec方法,获得DecorView的MeasureSpec。
其中getRootMeasureSpec方法中,根据自身LayoutParams的不同大小,共有三种情况:
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。- 固定大小(比如100dp):精确模式,大小为
LayoutParams中指定的大小。
对于普通View:
普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定的。
通过在父容器的measureChildWithMargins中的getChildMeasureSpec方法获得View的MeasureSpec。
其中getChildMeasureSpec方法的参数中使用到了父容器的MeasureSpec。在这个方法中通过switch和if语句,对View的MeasureSpec进行了决定。结果如下:
↓childLayoutParams \ parentMeasureSpec→ |
EXACTLY |
AT_MOST |
UNSPECIFIED |
|---|---|---|---|
dp/px |
EXACTLYchildSize |
EXACTLYchildSize |
EXACTLYchildSize |
match_parent |
EXACTLYparentSize |
AT_MOSTparentSize |
UNSPECIFIED0 |
wrap_content |
AT_MOSTparentSize |
AT_MOSTparentSize |
UNSPECIFIED0 |
上图可以简单记为:除了dp/px的精确模式情况外,三种MeasureSpec的级别为:EXACTLY < AT_MOST < UNSPECIFIED 且childMeasureSpec = max(childLayoutParams , parentMeasureSpec)。(实际上并无级别之分)
通过上面的表格,可以更加直观的看出:普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定的。
自定义View wrap_content不起作用的原因
对于直接继承View的自定义View来说,需要重写onMeasure方法并设置在wrap_content情况时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
因为当自定义View处于wrap_content时,它的SpecMode为AT_MOST模式,由前文可知,当父容器无论为AT_MOST还是ECACTLY模式时,自定义View都为AT_MOST模式且尺寸大小为parentSize,也就是父容器的剩余空间,即自定义View的大小变得与父容器的剩余空间大小一致,显然不是我们需要的。
解决方法:
解决方法也是非常简单的。我们只需要在onMeasure方法中对
- 仅
width方向为AT_MOST模式 - 仅
height方向为AT_MOST模式 width方向以及height方向都为AT_MOST模式- 其他的情况
这四个状态单独判断,分别使用setMeasuredDimension方法直接为自定义View设置我们需要的值即可。
源码中对于TextView、ImageView等控件的wrap_content也做了特殊的处理。
在Activity中获取某个View的宽高有几种方法
由于Activity的启动和View的测量过程并不是同步进行的,因此无法保证在Activity启动时某个View已经测量完毕了。所以我们在Activity启动的时候,如果直接通过getMeasuredWidth/getMeasuredHeight方法获得的值可能为0。
以下有四种方法可以解决这个问题(详细代码见《Android开发艺术探索》190页)。
Activity/View#onWindowFocusChanged在
Activity的onResume和onPause方法启动时,onWindowFocusChanged方法也会同时被调用,可以重写里面的内容,使之计算View的宽高。注意:
onWindowFocusChanged方法会在View已经初始化完毕后才开始调用。view.post(runnable)通过
post可以将一个runnable投递都消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。ViewTreeObserverViewTreeObserver中的众多回调可以完成这个功能。比如
OnGlobalLayoutListener接口,当View树的状态发生改变或者View树内部的View的可见性发生改变的时候,onGlobalLayout方法将被回调,此时就可以获取View的宽高了。view.measure(int widthMeasureSpec, int heightMeasureSpec)可以主动调用该方法开始计算
view的宽高。match_parent由于正常的
measure过程中,我们需要父容器的MeasureSpec的值,而此时父容器并没开始计算,我们无从得知这两个值的大小。故我们无法对match_parent的view计算其宽高。dp/px因为有精确的数值了,所以我们可以直接使用这个精确的数值通过
MeasureSpec.makeMeasureSpec方法构建view的MeasureSpec。wrap_content直接将
MeasureSpec.makeMeasureSpec方法中参数的值设置为(1<<30)-1即可,这是View理论上可以支持的最大值,所以这样构建MeasureSpec是合理的。
onCreate、onResume中可以获取View的宽高吗?怎么做?
View#post 为什么可以获取?
答案参考自:
onCreate、onResume中可以获取 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 | |
这样写一般是在 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#post与Handler#post的区别
答案参考自:
Handler.post,它的执行时间基本是等同于onCreate里那行代码触达的时间;View.post,则不同,它说白了执行时间一定是在
Activity#onResume发生后才开始算的;或者换句话说它的效果相当于你上面的View.post方法是写在Activity#onResume里面的(但只执行一次,因为onCreate不像onResume会被多次触发);当然,虽然这里说的是
post方法,但对应的postDelayed方法区别也是类似的。
getWidth()方法和getMeasureWidth()方法的区别
答案参考自:
getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。getMeasuredWidth()获取的是View原始的大小,也就是这个View在XML文件中配置或者是代码中设置的大小。getWidth()获取的是这个View最终显示的大小,这个大小有可能等于原始的大小,也有可能不相等。只要在代码里重新修改了子控件的摆放位置,getWidth()和getMeasureWidth()的值就会不同。getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
View加载流程(setContentView)
答案参考自:
通过Activity的setContentView方法间接调用PhoneWindow的setContentView(),在PhoneWindow中通过getLayoutInflate()得到LayoutInflate对象。
通过LayoutInflate对象去加载View,主要步骤是:
- 通过xml的Pull方式去解析xml布局文件,获取xml信息,并保存缓存信息,因为这些数据是静态不变的。
- 根据xml的tag标签通过反射创建View逐层构建View。
- 递归构建其中的子View,并将子View添加到父ViewGroup中。
其中,有四种加载XML文件的常用方法:
使用view的静态方法
1
View view = View.inflate(context, R.layout.child, null);通过系统获取
1
2LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.child, null);通过LayoutInflater
1
2LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.child, null);通过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方法只会**导致当前view的measure和layout**,而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
- invalidate怎么局部刷新
- Android绘制和屏幕刷新机制原理`