一次奇葩问题导致的彻底了解View.post()的底层原理

本篇文章已授权微信公众号 stormjun94 (Android 技术人员)独家发布

前言

原来一直以为View.post()就是简单的利用Handler发送出去,没有什么特殊的地方,没有必要纠结具体的实现,但是最近遇到一个问题,发现一个ListView中的一个View无法点击,特别奇怪显示正常,却无法点击,我跟踪源码最后发现一直执行到了ACTION_UP,也就是说这个事件是被这个View消费了,但是最后执行performClick()方法的时候,居然没有回调onClick的监听回调。

switch (action) {
                case MotionEvent.ACTION_UP:
                    {
                    if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //源码执行到了这个地方,但是没有回调onClick()
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                    }
 }

通过上面的源码,可以看到源码执行到了performClick的地方,但是最终没有回调onClick(onClickListener是存在的),这就让我不能理解了,唯一疑惑的地方就是这里,使用了post()方法,将performClick这个runnable发送了出去,所以让我纠结于是不是可以在View.post()里面研究一下。(本篇文章主要分析View.post的底层源码,关于这个问题,最后查源码,解决了,是ListView的itemView和viewType没有正确对应上的原因,但是能够正常显示,却不能点击,这里具体就不解释,后面如果有时间可以专门分析一下)

源码解析

View.post()不看不知道,一看才知道没有我们想象的那么容易。

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;
    }

可以看到这里,是存在两种情况的,一种attachInfo不为null的情况,一种是attachInfo为null的情况。这里就有几个问题了:

1.attachInfo是什么?
2.这两种情况有什么区别?
3.第二种没有直接通过Handler发出去,怎么执行的?

我上面的问题,是最后的查明原因的关键点,就是一个在ListView正常显示的View,但是它的attachInfo==null。那么就来一个问题一个问题解决吧。

1.AttachInfo是什么?

既然AttachInfo==null,那么我们肯定要追问,attachInfo什么时候赋值的,什么时候置空的,所以在View.java中全局搜索mAttachInfo =注意后面要带上一个空格,这也算一个看源码的小技巧吧,这样可以过滤很多无用的代码。

技巧一

可以看到整个代码里面一共就两处,而且对应的正好就是一处赋值,一处置空。

    /**
     * @param info the {@link android.view.View.AttachInfo} to associated with
     *        this view
     */
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
        ...
    }
void dispatchDetachedFromWindow() {
        ...

        mAttachInfo = null;
        ...
        }

可以看到方法名也很对称,一个对应于分发绑定,一个对应于分发解绑。所以看多了源码的应该能意识到这是Google常用的一种向下分发的机制,那么我们就要找到源头。
全局搜索dispatchAttachedToWindow,这里直接ctrl点击是没办法查看引用,所以这里还是要利用我们IDE的另一个功能全局查找。

一次奇葩问题导致的彻底了解View.post()的底层原理_第1张图片
技巧二

可以看到这里除了一些特殊的像RecyclerView这种特殊组件,一眼就可以看到一个很特殊的类ViewRootImpl.java,这不就是我们所有布局的最上层布局吗,那肯定就是它了。

private void performTraversals() {
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        }

这里就很清楚了,看到了我们很熟悉的一个方法,performTraversals(),这不就是我们页面绘制的起点吗,所以这里可以得出一条结论

在页面绘制的起点的时候,会通过分发的方式,将顶层的mAttachInfo分发给子View。而这个mAttachInfo是在ViewRootImpl初始化的时候构造函数中new出来的。

public ViewRootImpl(Context context, Display display) {
        ...
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
        ...
    }

可以看到,这里里面保存了很多基础信息,包括后面要使用的Handler对象。所以到这里我们第一个问题解决了。

2.这两种情况有什么区别?

通过上面的分析我们知道了,一个View在绘制到页面上后,都会被attach和当前页面绑定,对应的绑定的信息里面mAttachInfo有Handler对象(主线程Handler),还有其他对象,这也是为什么我们在自线程可以利用View.post执行UI操作的原因,因为要执行的操作会通过View内部的主线程Handler发到主线程执行。让我们再来看一下两种不同的场景。

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;
    }
场景一

如果一个正常的已经绘制到页面上的View,对应的mAttachInfo不会为null,所以当我们调用View.post的时候,会通过View内部的Handler对象,将runnable发送到主线程消息队列中执行。

场景二

对于场景二可能我们会很疑惑,没有见到执行的操作, 具体的场景这里举个例子,我们都知道,在Activity的onCreate方法中,我们可以通过View.post()拿到我们View的宽高,这是为什么呢?其实就是这个这里的场景二有关。
首先我们知道,在onCreate方法中,View还没有执行页面绘制的三大操作的,这也是我们为什么不能在onCreate拿到宽高的原因,因为页面的绘制流程的起点performTraversals()是在Activity的onResume方法之后执行的,所以这时候,当我们在onCreate方法中,执行View.post方法,根据前面的分析,没有执行performTraversals(),所以没有分发attach,所以mAttachInfo为null,这时候,就会执行我们的场景二。

private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
    
    
public class HandlerActionQueue {
    //数组
    private HandlerAction[] mActions;
    private int mCount;

    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    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++;
        }
    }
}

private static class HandlerAction {
        final Runnable action;
        final long delay;

        public HandlerAction(Runnable action, long delay) {
            this.action = action;
            this.delay = delay;
        }

        public boolean matches(Runnable otherAction) {
            return otherAction == null && action == null
                    || action != null && action.equals(otherAction);
        }
    }

我们这里可以看到,其实很简单,就是将我们要执行的runnable利用一个数组保存了起来,也就是说当我们在onCreate中执行View.post的时候,并没有立即执行我们要执行的方法,而是被保存了起来。那么这里场景二和场景一的区别其实也是很明显了,那么就到了最后一个问题。

3.第二种没有直接通过Handler发出去,怎么执行的?

通过上面我们知道,我们没执行的Runnable被保存了起来,在上面提到的HandlerActionQueue类中,我们找寻相关的方法,可以看到,一个关键方法(其实类很短,很好找,而且方法名也很直接)

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;
        }
    }

可以看到这里有个很明显的执行的方法,通过传入的Handler对象,遍历保存的数组,然后再将保存的runnable再通过handler发出去,传入到Handler对应的消息队列中。
这次先按住ctrl,看一下使用的类,发现有调用,一共有两处,一处和ViewRootIml有关一处和View有关。

一次奇葩问题导致的彻底了解View.post()的底层原理_第2张图片
技巧三

起初我没有多想,直接去看ViewRootIml的源码直接以为在这里就处理了,后来感谢@ 神天圣地的提醒,这里应该是在View的 dispatchAttachedToWindow分发的时候,才处理的,因为HandlerActionQueue对象是不一样的。

private void performTraversals() {
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
        getRunQueue().executeActions(mAttachInfo.mHandler);
        ...
        //执行绘制三大步骤
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
  // Transfer all pending runnables.
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
}

可以看到,这里又一次将mAttachInfo中的Handler传入,然后便会把我们通过View.post保存的runnable再发送到主线程的消息队列中,等待执行,由于后面里面会执行第一次到绘制步骤,所有,当执行到我们的runnable的时候,肯定就可以拿到View的宽高了。

特别注意

通过View.post拿到的宽高一定是真实的吗?

这个不一定,上面也提到了,这里只是第一次绘制的步骤,如果像RelativeLayout,或者其他特殊的View,再某些特殊情况下,会执行多次绘制,如果我们的runnable在第一次绘制结束后就里面执行,那么就拿到的只是第一次绘制结束后的宽高。当然,绝大部分拿到的是真实的宽高。

View的attach和detach一定只有ViewRootImpl执行吗?

不一定,例如一些特殊的存在组件复用的RecyclerView,都存在自己定制的attach和detach操作,具体可以看我写的关于RecyclerView的系列博客,可以让你深层次的了解RecyclerView。

总结

通过这次的分析,看似简单的View.post的分析过程其实涉及到了Handler机制,View的绘制流程等很多重要的知识点,现在看来还是很值得我们阅读这个的源码的。

相关博客推荐

1.【Android源码解析】View.post()到底干了啥 - 请叫我大苏 - 博客园

  1. 通过View.post()获取View的宽高引发的两个问题:1post的Runnable何时被执行,2为何View需要layout两次;以及发现Android的一个小bug

你可能感兴趣的:(一次奇葩问题导致的彻底了解View.post()的底层原理)