View.post到底做了什么

相信或多或少大家都使用过View的post一系列方法,来达到获取宽高或者做一些延迟操作什么的,不知道什么时候开始我突然觉得只会用,但是不了解为什么会这样,给我一种镜中花 水中月的感觉,所以有了下面的文章。

post几点疑问:

  • 从View.post开始,它到底做了什么?我为什么要用它?
  • 为什么可以获取到View的宽高?
  • 可不可以在Runnable中执行UI操作,可不可以在子线程中调用?
  • 会不会引发内存泄漏?

View 中包含三种post方法,分别是以下的三个方法。

public boolean post(Runnable action) {}
public boolean postDelayed(Runnable action, long delayMillis) {}
public void postOnAnimation(Runnable action) {}

当然平时我们用的最多的是前面两个,最后一个方法根据名称可以分析应该跟动画有关,最近在分析RecyclerView的源码和View的animate的源码时,看到了该方法。我会在最后提一下这个方法,接下来不多讲,先看View.post()到底做了什么?

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        return true;
    }

可以看到该方法获取一个mAttachInfo对象,会首先判断该对象是否为null来执行不同的步骤,当mAttachInfo不为null时,会调用mAttachInfo对象的Handler把Runnable post到消息队列中,熟悉Handler机制的我们这时就应该想到,这里的Handler究竟是不是主线程的Handler,如果是那么我们上面提到的最后两点疑问就可解决。因为Handler会将Runnable回调到主线程执行,而且Handler/Looper机制会导致内存泄漏。如果不了解的话,可以找一篇关于Handler的讲解去看一下看。
我们继续分析,会发现mAttachInfo==null时,会调用getRunQueue方法获取一个对象

 private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }

通过名字可以得出该对象和队列的用法应该类似,post方法中又会调用postDelayed方法,把传递进来的Runnable和time封装为HandlerAction对象存储到HandlerActionQueue中的mActions数组中等待执行。

接下来要考虑的就是mAttachInfo什么时候被赋值和mActions数组中的对象什么时候执行?恰好在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);
            }
。。。。。。
        }
    }

executeActions方法会遍历mActions中的对象并调用传递进来的Handler执行其中封装的Runnable对象。到这里就好办了直接查看哪里调用了该方法不就行了。通过代码追踪发现只有两处调用了该方法,分别是View 的dispatchAttachedToWindow方法以及ViewRootImpl的performTraversals方法。

不多说,接下来的分析根据找到的两处调用分别继续查找:

  1. dispatchAttachedToWindow方法的调用。
  2. performTraversals方法的调用。

首先从dispatchAttachedToWindow方法开始

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
     mAttachInfo = info;
     ......
     if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
     ......
 }

void dispatchDetachedFromWindow() {
      mAttachInfo = null;
    }

在最开始我们分析到mAttachInfo不为null时直接调用内部的Handler对象来执行 Runnable对象,而在View中经过全局搜索,只在上 面两处查到了mAttachInfo的赋值。在dispatchAttachedToWindow方法中赋值,在dispatchDetachedFromWindow方法中置空。
此处我省略了大部分代码,只留下相关的部分,可以看到在dispatchAttachedToWindow方法中首先为mAttachInfo赋值,然后在调用executeActions方法并且把AttachInfo中的Handler对象传递过去,而在哪里调用的dispatchAttachedToWindow方法呢 继续追踪代码,在ViewGroup中发现三处调用了View的dispatchAttachedToWindow方法。

  1. ViewGroup重写的dispatchAttachedToWindow中。
  2. ViewGroup的addViewInner方法。
  3. ViewGroup的addTransientView方法。

其实还有最后一处位于ViewRootImpl中的performTraversals方法内,该部分我们放在之前所说的第二个方向performTraversals中去讲,这里是先看ViewGroup中的调用。

@Override
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
      ......
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
      ......
    }

View Group本身继承自View。此处实现View的dispatchAttachedToWindow方法,并遍历内部View调用子View的dispatchAttachedToWindow方法来传递AttachInfo对象。而addViewInner和addTransientView都属于添加子View的操作。内部还是传递AttachInfo对象给子View。而dispatchAttachedToWindow中的AttachInfo对象是如何传递进来的肯定就不是走这里了。那么毫无疑问,最终的调用还是来自ViewRootImpl中的performTraversals方法。

接下来看的就是第二个方向ViewRootImpl中的performTraversals方法。

private void performTraversals() {
   final View host = mView;
   ......
   host.dispatchAttachedToWindow(mAttachInfo, 0);
   getRunQueue().executeActions(mAttachInfo.mHandler);
   ......
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
   performLayout(lp, mWidth, mHeight);
   performDraw();
   ......
 }

此处省略了大量代码 方便观看,最开始的mView,就是我们所熟知的DecordView,然后调用了view的dispatchAttachedToWindow方法,传递了mAttachInfo对象,到这里就差不多走通了。mAttachInfo对象是由DecordView传递给所有的子View,然后在子View的dispatchAttachedToWindow方法中调用executeActions方法通过Handler机制来执行之前存储的Runnable对象。而到底这里的Handler是不是主程序的Handler呢,我们继续追踪代码。

 mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);

在ViewRootImpl的构造函数中发现了mAttachInfo初始化的信息,看到了内部的mHandler了吗。我们继续往下看。

final ViewRootHandler mHandler = new ViewRootHandler();

可以发现是在ViewRootImpl中直接创建的一个final修饰的mHandler对象。没发现有传Looper,所以调用的是Handler的默认构造方法,通过sThreadLocal.get()来获取当前线程的Looper,而ViewRootImpl运行在主线程中,那么可以得出一个结论:

 mAttachInfo中传递的Handler就是主线程的handler

现在可以回答最开始疑问中的第三点了:可以在子线程中调用post方法。

接下来我们继续分析performTraversals方法,可能都注意到了一个比较有意思的点:dispatchAttachedToWindow方法是在measure、layout、draw之前调用,这就有一个疑问了:View都还没调用测量呢,我们怎么会获取到宽高呢?其实是可以理解的,因为Android是基于消息驱动的,当事件被添加到消息队列时,必须按照先后顺序执行,只有当前消息执行完了才会执行下一个事件。在这里或许有人明白了其实performTraversals本身就位于一个事件中。因此当HandlerActionQueue中缓存的Runnable被添加到消息队列时并不会立即执行。只有当前正在执行的事件结束也就是View被measure、layout、draw后,才会执行Runnable对象。到这里我们也可以回答了最开始提到的第二点疑问:为什么可以获取到View的宽高?

总结:

1.View.post()内部会根据mAttachInfo是否为null来两种情况。mAttachInfo!=null时,会直接执行Runnable对象的方法。等于null时,会将Runnable对象那个缓存到数组中,等待dispatchAttachedToWindow被调用,在执行。

2.所有的View中的mAttachInfo对象都是由ViewRootImpl中的performTraversals方法通过调用dispatchAttachedToWindow传递的一个mAttachInfo对象引用。

3.Runnable的执行是通过Android的Handler机制,将事件添加到消息队列中执行,最后提交到主线程中执行。

最后这里还要提一点,就是我们最开始列出来的postOnAnimation方法,该方法我在看View的animater源码时,发现是通过postOnAnimation方法来刷新View,实现View的属性动画,网上比较可信的理解是:等待下一个屏幕刷新信号来临时,刷新View。

你可能感兴趣的:(Android源码阅读系列)