View.post和Handler.post方法有区别吗

太长不看;先说结论:

  • 当 view 已经 attach 到 window 时,这两个方法没有区别
  • 当 view 还没 attach 到 window 时
    • View#post 方法接收的 Runnable 参数会被包装一层,然后加到队列。等到 view 被 attach 到 window 之后才会被加入到对应线程的 MessageQueue 中。这样可以保证调用时,view 已经执行过 measure layout draw 流程。此时在 Runnable 中可以拿到正确的宽高。
      • handler#post 方法,Runnable 会被直接添加到对应线程的 MessageQueue 中,然后被取出执行,跟 view 是否走完测量、布局、绘制 流程没有必然的联系,因此,可能会拿不到正确的宽高。

也就是说,如果我们要获取某个 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 方法

1
2
3
4
5
6
7
8
9
10
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

如果 mAttachInfo 不为空,那么直接使用其中的 mHandler ,调用其 post 方法。

否则,调用 HandlerActionQueue#post 方法,将它添加到 HandlerActionQueue 中。

HandlerActionQueue#post 方法最终会调用 postDelayed 方法

HandlerActionQueue#postDelayed

1
2
3
4
5
6
7
8
9
10
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}

看上面的实现,其实也比较简单,先新建一个 HandlerAction,将传入的 Runnable 包装起来,然后添加到 mActions 数组中。

可以看到这里仅仅是存储,什么时候会执行呢?

HandlerActionQueue#executeActions 会取出数组中的 HandlerAction 然后,调用 handler#postDelayed 将这些操作放到队列中执行

1
2
3
4
5
6
7
8
9
10
11
12
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}

HandlerActionQueue#executeActions 又是在什么时候调用的?

View#dispatchAttachedToWindow 方法中,会从 attachInfo 中取出 handler,然后调用 executeActions 方法

1
2
3
4
5
6
7
8
9
10
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
//省略一些代码
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue**.executeActions(info.mHandler);
mRunQueue = null;
}
//省略一些代码
}

dispatchAttachedToWindow 又是在什么时候调用的?

在 ViewRootImpl#performTraversals 方法中,会调用 DecorView#dispatchAttachedToWindow 方法,

DecorView 继承自 FrameLayout,而 FrameLayout 继承自ViewGroup,ViewGroup 重写了该方法,也就是调用 ViewGroup#dispatchAttachedToWindow。一层一层派发,遍历调用布局树中的每一个 view的 dispatchAttachedToWindow ,完成赋值。

ViewRootImpl#performTraversals

1
2
3
4
5
6
7
8
9
10
11
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
if (mFirst) {
//省略一些代码
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
}
//省略一些代码
}

注:上面方法中的 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,该方法内部有两个比较关键的调用
    1. mHandler.getLooper().getQueue().postSyncBarrier(); //插入消息屏障
      1. 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

注:插入消息屏障主要是为了让某些消息(比如 view Traversal 的消息)优先执行。

2、Handler#post 方法

如果 View 已经 attach 到布局树里面了,那么效果跟 Handler#post 方法是一样的。

Handler#post 原理比较简单,也就是设置对应的超时时间,加入到对应线程的 MessageQueue 中

由于本人水平有限,可能出于误解或者笔误难免出错,如果发现有问题或者对文中内容存在疑问请在下面评论区告诉我,谢谢!

Show Comments
0%