大巧无工,重剑无锋。
View 在 Android 开发中,有着极其重要的地位,甚至比常说的四大组件中的 BroadcastReceiver 和 ContentProvider 都重要。不知大家发现没有,在日常 App 的开发中,大部分时候都是和各种 View 打交道。这次,就来一探开发 View 中用到的 invalidate()、postInvalidate() 和 requestLayout() 的究竟。
引言
先从一道经典的面试题开始,“请描述 invalidate()、postInvalidate() 和 requestLayout() 的异同”,当初我甚至以为 invalidate() 是立即触发重绘,而 postInvalidate() 是延迟一段时间触发重绘,requestLayout() 都不知道是干嘛的,丢脸丢大发了。简略正确的回答应该大致如下:
I. Android 中实现 View 的更新有两种方法,一种是 invalidate(),另一种是 postInvalidate(),invalidate() 是在 UI 线程自身中使用,而 postInvalidate() 是在非 UI 线程中使用。
II. 当 View 确定自身不再适合现有的区域时,会调用 requestLayout() 方法要求 Parent View 重新调用其 onMeasure() 和 onLayout() 方法来重新设置自己的位置,尤其当 View 的 LayoutParams 发生改变,且其值尚未应用到 View 上时。
源码分析
I. invalidate()
调用该方法实现 View 树的重绘,常用于内部调用(如 setVisibility()) 或者需要刷新界面的时候,需要在主线程中(即 UI 线程)中调用该方法。
View#invalidate() 的源码如下:
1 | public void invalidate() { |
大体调用流程为:invalidate() — invalidateInternal() — p.invalidateChild() — ViewRootImpl 中的 invalidateChild() — invalidateChildInParent() — invalidateRectOnScreen() — scheduleTraversals()。
ViewRootImpl#scheduleTraversals() 的源码如下:
1 | void scheduleTraversals() { |
在 scheduleTraversals() 方法中,mTraversalRunnable 是一个 Runnable 对象:
1 | final class TraversalRunnable implements Runnable { |
另外,mChoreographer 是一个 Choreographer 对象,调用 postCallback() 方法,走到 ViewRootImpl 中的 scheduleFrameLocked() 方法,其内部通过 Handler 发送一个 Message 消息。
ViewRootImpl#scheduleFrameLocked() 的源码如下:
1 | private void scheduleFrameLocked(long now) { |
其对消息的处理如下:
1 | private final class FrameHandler extends Handler { |
doFrame() 中调用 doCallbacks() 方法,根据标志位,后来走到消息对象 mTraversalRunnable 的 run() 方法,在上文TraversalRunnable类中,run() 方法里会执行 doTraversal(),源码如下:
1 | void doTraversal() { |
该方法中会调用到 ViewRootImpl 中的 performTraversals() 方法,此方法为 View 绘图中第一个执行的方法,仔细看,方法中依次调用到 performMeasure()、performLayout() 和 performDraw() 方法来进行界面重绘。
II. postInvalidate()
该方法最终也是调用 invalidate() 进行重绘,其在子线程中,通过消息机制回调到主线程中,再调用 invalidate() 方法。
View#postInvalidate() 的源码如下:
1 | public void postInvalidate() { |
这是一个异步方法,在视图尚未被添加到窗口时通知重绘的话,会出现错误。故只有 attachInfo 不为 null 的时候会继续执行,即只有确保视图被添加到窗口的时候,才会通知 View 树重绘。其中,调用到dispatchInvalidateDelayed() 方法:
1 | public void dispatchInvalidateDelayed(View view, long delayMilliseconds) { |
通过 Handler 发送一个异步消息 MSG_INVALIDATE 到主线程,通知主线程刷新视图:
1 | final class ViewRootHandler extends Handler { |
很明显,调用到 invalidate() 方法,就继续 invalidate() 流程实现重绘。
III. requestLayout()
当动态移动一个 View 的位置,或者 View 的大小、形状发生变化时,调用该方法:
1 | view.requestLayout(); |
根据方法名字,“请求布局”即调用该方法时,子 View 会重新进行布局流程。然而,真实情况是子 View 调用该方法,会从 View 树重新进行一次测量、布局和绘制三个流程,再会显示子 View 的最终情况。那么,具体实现是什么呢?
首先,看 View#requestLayout() 的源码如下:
1 | // 从源码注释可以看出,如果当前 View 在请求布局的时候,View 树正在进行布局流程的话, |
首先,判断当前 View 树是否正在布局流程。接着,为当前子 View 设置标记位,标记当前的 View 需要重新布局。然后,调用 mParent.requestLayout() 方法,向父视图请求布局,即调用父视图的 requestLayout() 方法,为父视图添加 PFLAG_FORCE_LAYOUT 标记位。该父视图又会调用自己父视图的 requestLayout() 方法,即 requestLayout 事件层层向上传递,直到根 View (DecorView),最后会传递给 ViewRootImpl,也就是说,子 View 的 requestLayout 事件,最终会被 ViewRootImpl 接收并处理。综上所述,整个传递的流程是典型的责任连模式,即不断地向上传递事件,直到找到能处理该事件的上级,这里,ViewRootImpl 处理 requestLayout 事件,重写了 requestLayout() 方法。
ViewRootImpl#requestLayout() 的源码如下:
1 |
|
又走到了 scheduleTraversals() 方法!同样的分析又可以在上文提到的部分继续下去。
总结
如上流程图所示,一目了然。若 View 自身不再适用于当前区域,比如其 LayoutParams 发生改变,需要父布局对其重新测量、布局和绘制,往往使用 requestLayout()。而 invalidate() 或者 postInvalidate() 则是刷新当前 View,对当前的 View 进行重绘,不会进行测量与布局流程。因此,若 View 只需要重绘而不用测量和布局的时候,使用 invalidate() 比 requestLayout() 更加高效。
至此,从源码角度对 invalidate,postInvalidate 和 requestLayout 的剖析完毕。
本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。
1 | Email: [email protected] / WeChat: Wolverine623 |
您也可以关注我个人的微信公众号 :码农六哥,第一时间获得博客的更新通知,或后台留言与我交流。