Android-View.Post

本篇将介绍Android中的协程

View.Post

Android源码剖析

使用场景

1
2
3
view.post{
notifyAnimState(listener,ANIM_STATE_END)
}

众所周知,在子线程中是不能进行UI操作的,那么想要在子线程中进行UI操作,或者在很多场景下,一些操作想要延迟处理,该怎么办呢。答案就是都可以使用Handler来解决,但是说实话写Handler还是有点麻烦,还得注意内存泄漏问题。所以就可以使用View.post()或者View.postDelay()来替代Handler

但是用归用,难道就不好奇View.post为什么可以在子线程中操作UI,为什么可以解决在ActivityOnCreate()里调用View.post()来解决获取View高宽为0的问题。所以接下来将通过查看源码来解答以上两个问题

源码解析

View#

以下所有代码都处于View中

Post
1
2
3
4
5
6
7
8
9
10
11
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.post()可以在子线程中操作UI吗,其实跟我们想的差不多,post内部也是使用Handler来实现的。那么我们继续看看Android是怎么封装的

post里的代码还是蛮少的,我们慢慢来分析:

  1. 首先将成员变量mAttachInfo赋值给attachInfo,再判断attachInfo是否为空(也就是判断mAttachInfo是否为空)

    1
    2
    3
    4
    public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    ...
    }
  2. 如果attachInfo不为空的话,那么就直接调用Handler.post()Runnable传进去

    1
    2
    3
    4
    5
    6
    7
      public boolean post(Runnable action) {
    ...
    if (attachInfo != null) {
    return attachInfo.mHandler.post(action);
    }
    ...
    }

Runnable就是个接口,实现了Runnable接口的类,称为 线程辅助类Thread类才是真正的线程类,也就是最终还是想要通过Thread来创建线程并把线程辅助类传入进去

  1. 如果attachInfo为空的话,则执行从队列中获取一个值来执行Post。这里我们先不管这个队列中存储的是什么,是个什么队列,先继续往下看

    1
    2
    3
    4
      public boolean post(Runnable action) {
    ...
    getRunQueue().post(action);
    }
  2. 最终返回True,表示执行完成

View.post()里的代码已经看完了,但是又多出了几个新问题,也就是mAttachInfo到底是在哪里创建的,需要什么条件。getRunQueue又是啥,那么也就只能继续往下深入了

mAttachInfo

我们直接在Android Studio使用Ctrl + F来进行查找,在搜索的时候可以使用点小技巧,搜索mAttachInfo = (等号前后都有空格)。这样就可以直接查到两处赋值操作

1
2
3
4
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...
}
1
2
3
4
void dispatchDetachedFromWindow() {
...
mAttachInfo = null;
}

可以看到,这两处分别对应了赋值和置空,刚好是在对应的一个生命周期内。接下来就继续了解dispatchAttachedToWindowdispatchDetachedFromWindow分别在什么时候在哪里被调用了

好吧😛,其实可以先不用管它们两个在什么时候被调用了,至少在现在看来,在Activity.onCreate()的时候dispatchAttachedToWindow还是没有被调用的。但是我们在onCreate()里执行View.post里的操作仍然可以保证是在View的高宽计算完毕的,为什么呢?那么原因只可能在另一个return里了getRunQueue.post

getRunQueue
1
2
3
4
5
6
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}

这里的getRunQueue就是返回HandlerActionQueue,外部确实就是调用HandlerActionQueue.post,继续深入

HandlerActionQueue#

以下所有代码都处于HandlerActionQueue中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HandlerActionQueue {
private HandlerAction[] mActions; // 这里创建了一个HandlerAction数组
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];// 如果为空,则创建默认大小为4的数组
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
}

GrowingArrayUtils:从名字就可以看出来,是一个可以自动扩展的Array工具类

HandlerActionQueue.post内部则是调用了postDelayed()postDelayed()内部则是将Runnablelong作为参数创建一个HandlerAction,之后再添加到mActions数组中去,那就顺便看一下HandlerAction

HandlerAction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
}

蛮简单的一个类,主要作用就是把View.post传入的Runnable保存起来,如果是postDelay(),那就也把延迟时间用delay保存起来

现在我们回到上层,梳理一下。我们一开始调用的View.post传入的Runnable,在传到HandlerActionQueue里会包装成HanlderAction,然后再缓存起来。再HanlderActionQueue内部创建了个默认大小为4的HanlderAction数组,当数组不够用的时候,就会通过GrowingArrayUtils.append()来进行扩展。

总结大概流程:

Activity.onCreate –> 调用View.post() –> 还没进行dispatchAttachedToWindow –> HandlerActionQueue保存到数组中

那么什么时候这些存储起来的Runnable来会被执行呢,这又得回到HandlerActionQueue这里了

executeActions
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++) { // 全部遍历出来,然后加到Handler里面去
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}

mActions = null;
mCount = 0;
}
}

HandlerActionQueue里面,有一个executeActions的这么一个方法,就是用来处理HandlerAction的。还要一个主要的地方就是这些被缓存的Runnable也都是被Handler处理的,那么这个Hanlder是哪里来的呢

View#

以下所有代码都处于View中

dispatchAttachedToWindow
1
2
3
4
5
6
7
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}

从代码中很清晰的就可以看到,只要mRunQueue不为空,在执行dispatchAttachedToWindow的时候就会去运行

再次总结大概流程🎉:

Activity.onCreate –> 调用View.post() –> 还没进行dispatchAttachedToWindow –> 使用HandlerActionQueueRunnable保存到数组中 –> View.dispatchAttachedToWindow被执行 –> mRunQueue不为空 –> mRunQueue.executeActions()运行被储存的Runnable —>由于View.dispatchAttachedToWindow被执行,mAttachInfo不为空,则剩下的都交给mAttachInfo.mHandler

图片流程:

image-20210730143624882

到这里我们就可以回答一开始提的第二个问题,为什么可以解决在ActivityOnCreate()里调用View.post()来解决获取View高宽为0的问题。答案就是View先会将传入进来的Runnable都缓存起来,等到dispatchAttachedToWindow 执行,在先将缓存起来的Runnable遍历运行,之后的就正常交给mAttachInfo.mHandler。最后我们就只用搞清楚mAttachInfo是在哪里初始化的和什么时候会调用dispatchAttachedToWindow

通过查询,会在ViewRootlmpl.performTraversals()ViewGroup.addViewInner()里被调用

ViewGroup#

以下所有代码都处于ViewGroup 中

咱们先来看ViewGroup,在ViewGroupaddViewInner()方法内部会调用到dispatchAttachedToWindow

1
2
3
4
5
6
7
8
private void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) {
...
AttachInfo ai = mAttachInfo;
...
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
...
}
}

addViewInner()ViewGroup在添加View时内部的逻辑,也就是当ViewGroup.addView()的时候,如果mAttachInfo不为空,则会去调用子类的dispatchAttachedToWindow(),并将自身的AttachInfo传入过去。然后子View则会将传递过来的mAttachInfo赋值给自己的AttachInfo

那么ViewGroupAttachInfo是那来的呢,我们继续深入,最终找到的结果非常Amazed啊!。ViewGroup是继承View的,ViewGroup中的AttachInfo是直接使用ViewAttachInfo,可以很牛,直接绕回来了

1
2
3
4
5
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
...
AttachInfo mAttachInfo;
...
}

ViewGroup内部也有一个dispatchAttachedToWindow

1
2
3
4
5
6
7
8
9
10
11
12
@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
...
super.dispatchAttachedToWindow(info, visibility);
...
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
...
}

在该方法被调用时,会先调用父类的dispatchAttachedToWindow,也就是调用View.dispatchAttachedToWindowView中的AttachInfo也是在这个时候被赋值的),接着遍历所有子类,调用子类的dispatchAttachedToWindow,并传入自身的AttachInfo。但是这样一来,我们就直接进入循环了… 所以我们还是去看看ViewRootlmpl,希望那里会有答案

ViewRootlmpl#

以下所有代码都处于ViewRootlmpl中

performTraversals()
1
2
3
4
5
6
7
8
9
10
11
12
13
private void performTraversals() {
final View host = mView; //这里的View是Activity中的DecorView
...
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
...
}
mFirst = false;
...
}

在上述代码中,performTraversals()这个方法是用来通知ActivityView树来开始测量、布局、绘制。而DecorView就是Activity的根布局,View树的起点,是一个FrameLayout(是一个ViewGroup)。所以就是在这里第一次将mAttachInfo传给了DecorView中的所有子类

可以了,现在只用找到mAttachInfo是在哪里进行的初始化就可以了

1
2
3
4
5
public ViewRootImpl(Context context, Display display, IWindowSession session,boolean useSfChoreographer) {
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
...
}

找到了,在ViewRootImpl的一个构造方法中,创建了一个AttachInfo。所以说ViewRootImplAttachInfo的开头,AttachInfo是从这里开始往下传递的

但又有一个小问题出现了,那就是创建AttachInfo中放入的参数mHandlermHandler是那来的呢?

1
final ViewRootHandler mHandler = new ViewRootHandler();

mHandler是在内部被创建的,又因为ViewRootImpl是在主线程中,所以这里创建的Handler会自动的获取当前线程的Looper,所以也就可以回答一开始所问的问题,为什么可以在子线程中调用View.post来更新ui,因为内部的Handler始终都是在主线程上的

结论

当调用View.post的时候,View内部会先判断是否调用过dispatchAttachedToWindow,也就是mAttachInfo是否为空。为空的话,则会先将传入进来的Runnable通过调用HandlerActionQueue封装成HandlerAction保存在数组中,等待dispatchAttachedToWindow被执行。当dispatchAttachedToWindow被执行的时候,则会先将存储在HandlerActionQueue里的数组中的HandlerAction提取出来运行,之后的Runnable都将直接调用mAttachInfo.post()来运行

关于mAttachInfo的流程:一开始ViewRootlmpl通知ActivityView开始测量、布局、绘制。也就是通知Activity.DevorView去进行测量会调用view.dispatchAttachedToWindow()的方法,将自身的mAttachInfo传给根View。而根View(DevorView)是一个ViewGroup,所以在ViewGroupdispatchAttachedToWindow又会遍历子View,通过调用子ViewdispatchAttachedToWindowmAttachInfo传递给每个子View