Android invalidate/postInvalidate/requestLayout-彻底厘清

 我来答
舒适还明净的海鸥i
2022-06-30 · TA获得超过1.7万个赞
知道小有建树答主
回答量:380
采纳率:0%
帮助的人:69.7万
展开全部

系列文章:
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

已赞过 已踩过<
你对这个回答的评价是?
评论 收起
推荐律师服务: 若未解决您的问题,请您详细描述您的问题,通过百度律临进行免费专业咨询

为你推荐:

下载百度知道APP,抢鲜体验
使用百度知道APP,立即抢鲜体验。你的手机镜头里或许有别人想知道的答案。
扫描二维码下载
×

类别

我们会通过消息、邮箱等方式尽快将举报结果通知您。

说明

0/200

提交
取消

辅 助

模 式