相信或多或少大家都使用过View的post一系列方法,来达到获取宽高或者做一些延迟操作什么的,不知道什么时候开始我突然觉得只会用,但是不了解为什么会这样,给我一种镜中花 水中月的感觉,所以有了下面的文章。
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方法。
不多说,接下来的分析根据找到的两处调用分别继续查找:
首先从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方法。
其实还有最后一处位于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。