Android invalidate/postInvalidate/requestLayout-彻底厘清
系列文章:
Android Activity创建到View的显示过程
Android Activity 与View 的互动思考
Android invalidate/postInvalidate/requestLayout-彻底厘清
Android 容易遗漏的刷新小细节
前几篇分析了Measure、Layout、Draw 过程,这三个过程在第一次展示View的时候都会调用。那之后更改了View的属性呢?比如更改颜色、更换文字内容、更换图片等,还会走这三个过程吗?循着这个思路,来分析Invalidate/RequestLayout流程。
通过本篇文章,你将了解到:
MyView 默认展示一块红色的矩形区域,暴露给外界的方法:setColor
用以改变绘制的颜色。颜色改变后,需要重新执行onDraw(xx)才能看到改变后的效果,通过invalidate()方法触发onDraw(xx)调用。
接下来看看invalidate()方法是怎么触发onDraw(xx)方法执行的。
invalidate顾名思义:使某个东西无效。在这里表示使当前绘制内容无效,需要重新绘制。当然,一般来说常常简单称作:刷新。
invalidate()是View.java 里的方法。
从上可知,当前要刷新的View确定了刷新区域后即调用了父布局的invalidateChild(xx)方法。该方法为ViewGroup里的final方法。
由上可知,在该方法里区分了硬件加速绘制与软件绘制,分别来看看两者区别:
硬件加速绘制分支
如果该Window支持硬件加速,则走下边流程:
onDescendantInvalidated 方法的目的是不断向上寻找其父布局,并将父布局PFLAG_DRAWING_CACHE_VALID 标记清空,也就是绘制缓存清空。
而我们知道,根View的mParent指向ViewRootImpl对象,因此来看看它里面的onDescendantInvalidated()方法:
做个小结:
用图表示硬件加速绘制的invaldiate流程:
软件绘制分支
如果该Window不支持硬件加速,那么走软件绘制分支:
parent.invalidateChildInParent(location, dirty) 返回mParent,只要mParent不为空那么一直调用invalidateChildInParent(xx),实际上这也是遍历ViewTree过程,来看看关键invalidateChildInParent(xx):
与硬件加速绘制一致,最终调用ViewRootImpl invalidateChildInParent(xx),来看看实现:
做个小结:
用图表示软件绘制invalidate流程:
上述分析了硬件加速绘制与软件绘制时invalidate的不同,它们的最终目的都是为了重走Draw过程。重走Draw过程通过调用scheduleTraversals() 触发的,来看看是如何触发的。
想了解更多硬件加速绘制请移步:
Android 自定义View之Draw过程(中)
触发Draw过程
scheduleTraversals 详细分析在这篇文章:
Android Activity创建到View的显示过程
三大流程真正开启在ViewRootImpl->performTraversals(),在该方法里根据一定的条件执行了Measure(测量)、Layout(摆放)、Draw(绘制)。
本次着重分析如何触发Draw过程。
可以看出,invalidate 最终触发了Draw过程。
可以看出,启用硬件加速绘制可以避免不必要的绘制。
关于硬件加速绘制与软件绘制详细区别,请移步系列文章:
Android 自定义View之Draw过程(上)
最后,用图表示invalidate流程:
顾名思义,重新请求布局。
来看看View.requestLayout()方法:
可以看出,这个递归调用和invalidate一样的套路,向上寻找其父布局,一直到ViewRootImpl为止,给每个布局设置PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED标记。
查看ViewRootImpl requestLayout()
很明显,requestLayout目的很单纯:
和invalidate一样的配方,当刷新信号来到之时,调用doTraversal()->performTraversals(),而在performTraversals()里真正执行三大流程。
由此可见:
之前设置的PFLAG_FORCE_LAYOUT标记有啥用呢?
回忆一下measure 过程:
PFLAG_FORCE_LAYOUT 标记打上之后,会触发onMeasure()测量自身及其子布局。
试想一下,假设View的尺寸改变了,变大了,那么调用了requestLayout后因为走了Measure、Layout 过程,测量、摆放倒是重新设置了,但是不调用Draw出不来效果啊。实际上,View layout时候已经考虑到了。
在View.layout(xx)->setFrame(xx)里
也就是说:
关于measure、layout 过程更深入的分析,请移步:
用图表示requestLayout过程:
结合requestLayout和invalidate与View三大流程关系,有如下图:
总结一下:
上面仅仅说明了单个布局Invalidate/RequestLayout联系,那么如果父布局调用了invalidate,那么子布局会走重绘过程吗?接下来列举这些关系。
子布局Invalidate
如果是软件绘制或者父布局开启了软件缓存绘制,父布局会走重绘过程(前提是WILL_NOT_DRAW标记没设置)。
子布局RequestLayout
父布局会重走Measure、Layout过程。
父布局Invalidate
如果是软件绘制,则子布局会走重绘过程。
父布局RequestLayout
如果父布局尺寸发生了改变,则会触发子布局Measure过程、Layout过程。
在Activity onCreate里创建子线程并展示对话框:
答案是可以的,接下来分析为什么可以。
在分析ViewRootImpl里requestLayout/invalidate过程中,发现其内部调用了checkThread()方法:
问题的关键是mThread是什么?从哪里来?
而创建ViewRootImpl对象是在调用WindowManager.addView(xx)过程中创建的。
关于WindowManager/Window 请移步: Window/WindowManager 不可不知之事
现在回过头来看Dialog创建就比较明朗了:
实际上,"子线程不能更新ui" 更合理的表述应为:View只能被构建了ViewTree的线程操作。只是通常来说,Activity 构建ViewTree的线程被称作UI(主)线程,因此才会有上述说法。
既然invalidate()只能主线程调用(硬件加速条件下,不调用checkThread()),那如果想在子线程调用呢?当然想到的是先通过Handler切换到主线程,再执行invalidate(),但是每次这么写有点冗余,幸好,View里提供了postInvalidate:
切到ViewRootImpl.java
发现了真相:
本文基于Android 10.0