太长不看;先说结论:
- 当 view 已经 attach 到 window 时,这两个方法没有区别
- 当 view 还没 attach 到 window 时
- View#post 方法接收的 Runnable 参数会被包装一层,然后加到队列。等到 view 被 attach 到 window 之后才会被加入到对应线程的 MessageQueue 中。这样可以保证调用时,view 已经执行过 measure layout draw 流程。此时在 Runnable 中可以拿到正确的宽高。
- handler#post 方法,Runnable 会被直接添加到对应线程的 MessageQueue 中,然后被取出执行,跟 view 是否走完测量、布局、绘制 流程没有必然的联系,因此,可能会拿不到正确的宽高。
- View#post 方法接收的 Runnable 参数会被包装一层,然后加到队列。等到 view 被 attach 到 window 之后才会被加入到对应线程的 MessageQueue 中。这样可以保证调用时,view 已经执行过 measure layout draw 流程。此时在 Runnable 中可以拿到正确的宽高。
也就是说,如果我们要获取某个 view (称为 viewA)的宽高,最好是通过 viewA#post 方法,不然很有可能因为 view 的测量、布局流程还没走完。导致拿到错误的宽高。
注:「已经 attach 到 window」指的是已经执行完 View#dispatchAttachedToWindow 方法,也就是 View 的 mAttachInfo 成员变量值不为空。
问题背景
做需求开发的时候,需要对某个页面的 UI 进行改造,改造完之后发现,原本头部根据上下滑动的距离 透明度渐变的效果会偶现失效。
渐变实现原理:使用纵向滑动的距离除以头部的高度得出透明度。
因为是透明度失效,因此看下对应值是否正常。断点查看发现,当渐变效果失效的时候,头部的高度值为 0,该值作为除数,得到的结果是无限大,从而导致透明度渐变失效。
为什么拿到的高度会为 0 呢?
因为高度是通过 handler#post 方法,在 Runnable 中获取的。使用 handler#post 去获取 view 的宽度。此时 view 有可能还没有走完测量、布局流程,就会导致拿到的高度为 0,进而出现上下滑动,标题栏的透明度也不改变的 bug。
解决方案
将获取 view 高度的方式,从 hander#post 修改为 view#post 即可。
流程简析
以下源码基于 API-29
1、View#post 方法
|
|
如果 mAttachInfo 不为空,那么直接使用其中的 mHandler ,调用其 post 方法。
否则,调用 HandlerActionQueue#post 方法,将它添加到 HandlerActionQueue 中。
HandlerActionQueue#post 方法最终会调用 postDelayed 方法
HandlerActionQueue#postDelayed
|
|
看上面的实现,其实也比较简单,先新建一个 HandlerAction,将传入的 Runnable 包装起来,然后添加到 mActions 数组中。
可以看到这里仅仅是存储,什么时候会执行呢?
HandlerActionQueue#executeActions 会取出数组中的 HandlerAction 然后,调用 handler#postDelayed 将这些操作放到队列中执行
|
|
HandlerActionQueue#executeActions 又是在什么时候调用的?
View#dispatchAttachedToWindow 方法中,会从 attachInfo 中取出 handler,然后调用 executeActions 方法
|
|
dispatchAttachedToWindow 又是在什么时候调用的?
在 ViewRootImpl#performTraversals 方法中,会调用 DecorView#dispatchAttachedToWindow 方法,
DecorView 继承自 FrameLayout,而 FrameLayout 继承自ViewGroup,ViewGroup 重写了该方法,也就是调用 ViewGroup#dispatchAttachedToWindow。一层一层派发,遍历调用布局树中的每一个 view的 dispatchAttachedToWindow ,完成赋值。
ViewRootImpl#performTraversals
|
|
注:上面方法中的 mView 其实是 DecorView。(可以看看 Activity#makeVisible 方法,该方法会调用 ViewRootImpl#setView 方法,将 DecorView 设置进来)
为什么使用 View#post,能够拿到 view 的宽高呢?
从 HandlerActionQueue#executeActions 的实现可以看到,这些 Runnable,最终也是通过 handler#postDelay 方法执行的。也就是说,它们也会被加入到对应线程的 MessageQueue 中,排队执行。
此时,主线程执行到的是 performTraversals 方法,内部还会再去调用 performMeasure、performLayout、performDraw 三个方法。
而 measure 和 layout 完成之后,view 的宽高也就确定了。
由于我们的 Runnable 是在这个步骤之后才会被执行,所以,可以拿到正确的宽高。
ViewRootImpl#performTraversal 又是什么时候调用的?
我们看回 ViewRootImpl 中,其中有一个内部类 TraversalRunnable,在其 run 方法中会调用 doTraversal,doTraversal 内部会调用 performTraversals(); 同时 ViewRootImpl 中 mTraversalRunnable 成员变量。在 scheduleTraversals 和 unscheduleTraversals 方法中会用到该成员变量
大体流程如下:
- 当我们修改了 view 的属性,然后调用 invalidate 或者是 requestLayout 方法,都会触发 ViewRootImpl#scheduleTraversals,该方法内部有两个比较关键的调用
mHandler.getLooper().getQueue().postSyncBarrier();//插入消息屏障mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);- mChoreographer#postCallback 最终调用到 postCallbackDelayedInterna ,该方法内部会将 mTraversalRunnable 添加到 Choreographer 的 CallbackQueue 中
- 最终调用到 DisplayEventReceiver#scheduleVsync 。相当于注册监听了一个 vsync 回调
- 当垂直同步信号回来调用到 Choreographer.FrameDisplayEventReceiver#onVsync,发送一条异步定时消息,最终会调用该类的 run 方法。
- 执行 FrameDisplayEventReceiver#run
- 触发 Choreographer#doFrame
- doFrame 内部会取出 CallbackQueue 中的任务执行,比如我们之前的 mTraversalRunnable
- 然后触发 doTraversal
- 最后触发 performTraversal
- 然后触发 doTraversal
- doFrame 内部会取出 CallbackQueue 中的任务执行,比如我们之前的 mTraversalRunnable
- 触发 Choreographer#doFrame
- 执行 FrameDisplayEventReceiver#run
- 当垂直同步信号回来调用到 Choreographer.FrameDisplayEventReceiver#onVsync,发送一条异步定时消息,最终会调用该类的 run 方法。
- 最终调用到 DisplayEventReceiver#scheduleVsync 。相当于注册监听了一个 vsync 回调
- mChoreographer#postCallback 最终调用到 postCallbackDelayedInterna ,该方法内部会将 mTraversalRunnable 添加到 Choreographer 的 CallbackQueue 中
注:插入消息屏障主要是为了让某些消息(比如 view Traversal 的消息)优先执行。
2、Handler#post 方法
如果 View 已经 attach 到布局树里面了,那么效果跟 Handler#post 方法是一样的。
Handler#post 原理比较简单,也就是设置对应的超时时间,加入到对应线程的 MessageQueue 中
由于本人水平有限,可能出于误解或者笔误难免出错,如果发现有问题或者对文中内容存在疑问请在下面评论区告诉我,谢谢!