View.post() 原理深入理解

关于 View.post() 相信每个 Android 开发人员都不会感到陌生,它最常见的场景主要有两种。

  1. 更新 UI 操作

  2. 获取 View 的实际宽高

view.post() 的内部也是调用了 Handler,这可能是绝大多数开发人员所了解的,从本质来说这样理解并没有错,不过它并能解释上面提出的第 2 个场景。

在 Activity 中,View 绘制流程的开始时机是在 ActivityThread 的 handleResumeActivity 方法,在该方法首先完成 Activity 生命周期 onResume 方法回调,然后开始 View 绘制任务。也就是说 View 绘制流程要在 onResume 方法之后,但是我们绝大部分业务是在 onCreate 方法,比如要获取某个 View 的实际宽高,由于 View 的绘制任务还未开始,所以就无法正确获取。

此时大家肯定使用过 View.post() 来解决该问题,注意 View 绘制流程也是向 Handler 添加任务,如果在 onCreate 方法直接使用 Handler.post(),则该任务一定在 View 绘制任务之前(同一个线程队列机制)。

  • 注意这里不考虑使用 ViewTreeObserver 或更长延迟的 postDelayed()。

那 View.post() 内部也是使用 Handler,它是如何实现的呢?简单来说,View.post() 对任务的运行时机做了调整。


View.post()

翻开 View 源码,找到 View 的 post 方法如下:

public boolean post(Runnable action) {
    // 首先判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果不为null,直接调用其内部Handler的post
        return attachInfo.mHandler.post(action);
    }

    // 否则加入当前View的等待队列
    getRunQueue().post(action);
    return true;
}

注意 AttachInfo 是 View 的静态内部类,每个 View 都会持有一个 AttachInfo,它默认为 null;需要先来看下 getRunQueue().post():

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

getRunQueue() 返回的是 HandlerActionQueue,也就是调用了 HandlerActionQueue 的 post 方法:

public void post(Runnable action) {
    // 调用到postDelayed方法,这有点类似于Handler发送消息
    postDelayed(action, 0);
}

// 实际调用postDelayed
public void postDelayed(Runnable action, long delayMillis) {
    // HandlerAction表示要执行的任务
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            // 创建一个保存HandlerAction的数组
            mActions = new HandlerAction[4];
        }
        // 表示要执行的任务HandlerAction 保存在 mActions 数组中
        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        // mActions数组下标位置累加1
        mCount++;
    }
}

HandlerAction 表示一个待执行的任务,内部持有要执行的 Runnable 和延迟时间;类声明如下:

private static class HandlerAction {
    // post的任务
    final Runnable action;
    // 延迟时间
    final long delay;

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

    // 比较是否是同一个任务
    // 用于匹配某个 Runnable 和对应的HandlerAction
    public boolean matches(Runnable otherAction) {
        return otherAction == null && action == null
                || action != null && action.equals(otherAction);
    }
}

注意 postDelayed() 创建一个默认长度为 4 的 HandlerAction 数组,用于保存 post() 添加的任务;跟踪到这,大家是否有这样的疑惑:View.post() 添加的任务没有被执行?

实际上,此时我们要回过头来,重新看下 AttachInfo 的创建过程,先看下它的构造方法:

AttachInfo(IWindowSession session, IWindow window, Display display,
               ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
               Context context) {
        mSession = session;
        mWindow = window;
        mWindowToken = window.asBinder();
        mDisplay = display;
        // 持有当前ViewRootImpl
        mViewRootImpl = viewRootImpl;
        // 当前渲染线程Handler
        mHandler = handler;
        mRootCallbacks = effectPlayer;
        // 为其创建一个ViewTreeObserver
        mTreeObserver = new ViewTreeObserver(context);
    }

注意 AttachInfo 中持有当前线程的 Handler。翻阅 View 源码,发现仅有两处对 mAttachInfor 赋值操作,一处是为其赋值,另一处是将其置为 null。

  • mAttachInfo 赋值过程:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 给当前View赋值AttachInfo,此时所有的View共用同一个AttachInfo(同一个ViewRootImpl内)
    mAttachInfo = info;
    // View浮层,是在Android 4.3添加的
    if (mOverlay != null) {
        // 任何一个View都有一个ViewOverlay
        // ViewGroup的是ViewGroupOverlay
        // 它区别于直接在类似RelativeLaout/FrameLayout添加View,通过ViewOverlay添加的元素没有任何事件
        // 此时主要分发给这些View浮层
        mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
    }
    mWindowAttachCount++;

     // ... 省略

    if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER) != 0) {
        mAttachInfo.mScrollContainers.add(this);
        mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
    }
    //  mRunQueue,就是在前面的 getRunQueue().post()
    // 实际类型是 HandlerActionQueue,内部保存了当前View.post的任务
    if (mRunQueue != null) {
        // 执行使用View.post的任务
        // 注意这里是post到渲染线程的Handler中
        mRunQueue.executeActions(info.mHandler);
        // 保存延迟任务的队列被置为null,因为此时所有的View共用AttachInfo
        mRunQueue = null;
    }
    performCollectViewAttributes(mAttachInfo, visibility);
    // 回调View的onAttachedToWindow方法
    // 在Activity的onResume方法中调用,但是在View绘制流程之前
    onAttachedToWindow();

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
            // 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小
            listener.onViewAttachedToWindow(this);
        }
    }

    // ...  省略

    // 回调View的onVisibilityChanged
    // 注意这时候View绘制流程还未真正开始
    onVisibilityChanged(this, visibility);

    // ... 省略
}

方法最开始为当前 View 赋值 AttachInfo。注意 mRunQueue 就是保存了 View.post() 任务的 HandlerActionQueue;此时调用它的 executeActions 方法如下:

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中,等待执行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        //此时不在需要,后续的post,将被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}

遍历所有已保存的任务,发送到 Handler 中排队执行;将保存任务的 mActions 置为 null,因为后续 View.post() 直接添加到 AttachInfo 内部的 Handler 。所以不得不去跟踪 dispatchAttachedToWindow() 的调用时机。

ViewRootImpl

同一个 View Hierachy 树结构中所有 View 共用一个 AttachInfo,AttachInfo 的创建是在 ViewRootImpl 的构造方法中:

mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
  • 一般 Activity 包含多个 View 形成 View Hierachy 的树形结构,只有最顶层的 DecorView 才是对 WindowManagerService “可见的”。

dispatchAttachedToWindow() 的调用时机是在 View 绘制流程的开始阶段。在 ViewRootImpl 的 performTraversals 方法,在该方法将会依次完成 View 绘制流程的三大阶段:测量、布局和绘制,不过这部分不是今天要分析的重点。

// View 绘制流程开始在 ViewRootImpl
private void performTraversals() {
    // mView是DecorView
    final View host = mView;
    if (mFirst) {
        .....
        // host为DecorView
        // 调用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 给子view
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
        .....
    } 
   mFirst=false
   ...
   // Execute enqueued actions on every traversal in case a detached view   enqueued an action
   getRunQueue().executeActions(mAttachInfo.mHandler);
   // View 绘制流程的测量阶段
   performMeasure();
   // View 绘制流程的布局阶段
   performLayout();
   // View 绘制流程的绘制阶段
   performDraw();
   ...

}

host 的实际类型是 DecorView,DecorView 继承自 FrameLayout。

  • 每个 Activity 都有一个关联的 Window 对象,用来描述应用程序窗口,每个窗口内部又包含一个 DecorView 对象,DecorView 对象用来描述窗口的视图 — xml 布局。通过 setContentView() 设置的 View 布局最终添加到 DecorView 的 content 容器中。

跟踪 DecorView 的 dispatchAttachedToWindow 方法的执行过程,DecorView 并没有重写该方法,而是在其父类 ViewGroup 中:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    // 子View的数量
    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍历所有子View
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        // 遍历调用所有子View的dispatchAttachedToWindow
        // 为每个子View关联AttachInfo
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    // ...
}

for 循环遍历当前 ViewGroup 的所有 childView,为其关联 AttachInfo。子 View 的 dispatchAttachedToWindow 方法在前面我们已经分析过了:首先为当前 View 关联 AttachInfo,然后将之前 View.post() 保存的任务添加到 AttachInfo 内部的 Handler

注意回到 ViewRootImpl 的 performTraversals 方法,咋一看,这个过程好像没有太多新奇的地方。不过你是否注意到这一过程是在 View 的绘制任务中。

通过 View.post() 添加的任务,是在 View 绘制流程的开始阶段,将所有任务重新发送到消息队列的尾部,此时相关任务的执行已经在 View 绘制任务之后,即 View 绘制流程已经结束,此时便可以正确获取到 View 的宽高了

View.post() 添加的任务能够保证在所有 View(同一个 View Hierachy 内) 绘制流程结束之后才被执行

碎片化问题来了,如果我们只是创建一个 View,调用它的 post 方法,它会不会被执行呢?代码如下:

final ImageView view = new ImageView(this);
    view.post(new Runnable() {
        @Override
        public void run() {
            // do something
        }
    });

答案是否定的,因为它没有添加到窗口视图,不会走绘制流程,自然也就不会被执行。此时只需要添加如下代码即可:

// 将View添加到窗口
// 此时重新发起绘制流程,post任务会被执行
contentView.addView(view);

不过该问题在 API Level 24 之前不会发生,看下之前的代码实现:

// API Level 24之前的post实现
public boolean post(Runnable action) {
    // 这里的逻辑与API Level 24及以后一致
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // 主要是这里,此时管理待执行的任务直接交给了 ViewRootImpl 中。
    // 而在API Level 24及以后,每个View自行维护待执行任务队列,
    // 故,如果View不添加到Window视图,dispatchAttachedToWindow 不会被调用,
    // View中的post任务将永远得不到执行
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

在 API Level 24 之前,通过 View.post() 任务被直接添加到 ViewRootImpl 中,在 24 及以后,每个 View 自行维护待执行的 post() 任务,它们要依赖于 dispatchAttachedToWindow 方法,如果 View 未添加到窗口视图,post() 添加的任务将永远得不到执行

这样的碎片化问题在 Android 中可能数不胜数,这也告诫我们如果对某项功能点了解的不够充分,最后可能导致程序未按照意愿执行。

至此,View.post() 的原理我们就算搞清楚了,不过还是有必要跟踪下 AttachInfo 的释放过程。

  • mAttachInfo 置 null 的过程:

先看下表示 DecorView 的 dispatchDetachedFromWindow 方法,实际是调用其父类 ViewGroup 中:

// ViewGroup 的 dispatchDetachedFromWindow
void dispatchDetachedFromWindow() {

    // ... 省略

    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍历所有childView
    for (int i = 0; i < count; i++) {
        // 通知childView dispatchDetachedFromWindow
        children[i].dispatchDetachedFromWindow();
    }

    // ... 省略

    super.dispatchDetachedFromWindow();
}

不出所料 ViewGroup 的 dispatchDetachedFromWindow 方法会遍历所有 childView。

void dispatchDetachedFromWindow() {
    AttachInfo info = mAttachInfo;
    if (info != null) {
        int vis = info.mWindowVisibility;
        if (vis != GONE) {
            // 通知 Window显示状态发生变化
            onWindowVisibilityChanged(GONE);
            if (isShown()) {
                onVisibilityAggregated(false);
            }
        }
    }
    // 回调View的onDetachedFromWindow
    onDetachedFromWindow();
    onDetachedFromWindowInternal();

    // ... 省略

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知回调 onViewDetachedFromWindow
            listener.onViewDetachedFromWindow(this);
        }
    }

    // ... 省略

    // 将AttachInfo置为null
    mAttachInfo = null;
    if (mOverlay != null) {
        // 通知浮层View
        mOverlay.getOverlayView().dispatchDetachedFromWindow();
    }

    notifyEnterOrExitForAutoFillIfNeeded(false);
}

可以看到在 dispatchDetachedFromWindow 方法,首先回调 View 的 onDetachedFromWindow(),然后通知所有监听者 onViewDetachedFromWindow(),最后将 mAttachInfo 置为 null。

由于 dispatchAttachedToWindow 方法是在 ViewRootImpl 中完成,此时很容易想到它的释放过程肯定也在 ViewRootImpl,跟踪发现如下调用过程:

void doDie() {
    // 检查执行线程
    checkThread();

    synchronized (this) {
        if (mRemoved) {
            return;
        }
        mRemoved = true;
        if (mAdded) {
            // 回调View的dispatchDetachedFromWindow
            dispatchDetachedFromWindow();
        }

        if (mAdded && !mFirst) {
            destroyHardwareRenderer();

            // mView是DecorView
            if (mView != null) {
                int viewVisibility = mView.getVisibility();
                // 窗口状态是否发生变化
                boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                if (mWindowAttributesChanged || viewVisibilityChanged) {
                    try {
                        if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                            mWindowSession.finishDrawing(mWindow);
                        }
                    } catch (RemoteException e) {
                    }
                }
                // 释放画布
                mSurface.release();
            }
        }

        mAdded = false;
    }

    // 将其从WindowManagerGlobal中移除
    // 移除DecorView
    // 移除DecorView对应的ViewRootImpl
    // 移除DecorView
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

可以看到 dispatchDetachedFromWindow 方法被调用,注意方法最后将 ViewRootImpl 从 WindowManager 中移除。

经过前面的分析我们已经知道 AttachInfo 的赋值操作是在 View 绘制任务的开始阶段,而它的调用者是 ActivityThread 的 handleResumeActivity 方法,即 Activity 生命周期 onResume 方法之后。

那它是在 Activity 的哪个生命周期阶段被释放的呢?在 Android 中, Window 是 View 的容器,而 WindowManager 则负责管理这些窗口,具体可以参考《View 绘制流程之 DecorView 添加至窗口的过程》。

我们直接找到管理应用进程窗口的 WindowManagerGlobal,查看 DecorView 的移除工作:

/**
 * 将DecorView从WindowManager中移除
 */
public void removeView(View view, boolean immediate) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }

    synchronized (mLock) {
        // 找到保存该DecorView的下标,true表示找不到要抛出异常
        int index = findViewLocked(view, true);
        // 找到对应的ViewRootImpl,内部的DecorView
        View curView = mRoots.get(index).getView();
        // 从WindowManager中移除该DecorView
        // immediate 表示是否立即移除
        removeViewLocked(index, immediate);
        if (curView == view) {
            // 判断要移除的与WindowManager中保存的是否为同一个
            return;
        }

        // 如果不是同一个View(DecorView),抛异常
        throw new IllegalStateException("Calling with view " + view
                + " but the ViewAncestor is attached to " + curView);
    }
}

根据要移除的 DecorView 找到在 WindowManager 中保存的 ViewRootImpl,真正移除是在 removeViewLocked 方法:

private void removeViewLocked(int index, boolean immediate) {
    // 找到对应的ViewRootImpl
    ViewRootImpl root = mRoots.get(index);
    // 该View是DecorView
    View view = root.getView();

    // ... 省略
    
    // 调用ViewRootImpl的die
    // 并且将当前ViewRootImpl在WindowManagerGlobal中移除
    boolean deferred = root.die(immediate);
    if (view != null) {
        // 断开DecorView与ViewRootImpl的关联
        view.assignParent(null);
        if (deferred) {
            // 返回 true 表示延迟移除,加入待死亡队列
            mDyingViews.add(view);
        }
    }
}

可以看到调用了 ViewRootImpl 的 die 方法,回到 ViewRootImpl 中:

boolean die(boolean immediate) {
    // immediate 表示立即执行
    // mIsInTraversal 表示是否正在执行绘制任务
    if (immediate && !mIsInTraversal) {
        // 内部调用了View的dispatchDetachedFromWindow
        doDie();
        // return false 表示已经执行完成
        return false;
    }

    if (!mIsDrawing) {
        // 释放硬件加速绘制
        destroyHardwareRenderer();
    } 
    // 如果正在执行遍历绘制任务,此时需要等待遍历任务完成
    // 故发送消息到尾部
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

注意 doDie 方法(源码在前面已经贴出),它最终会调用 dispatchDetachedFromWindow 方法。

最后,移除 Window 窗口任务是通过 ActivityThread 完成的,具体调用在 handleDestoryActivity 方法完成:

private void handleDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    // 回调 Activity 的 onDestory 方法
    ActivityClientRecord r = performDestroyActivity(token, finishing,
            configChanges, getNonConfigInstance);
    if (r != null) {
        cleanUpPendingRemoveWindows(r, finishing);

        // 获取当前Window的WindowManager, 实际是WindowManagerImpl
        WindowManager wm = r.activity.getWindowManager();
        // 当前Window的DecorView
        View v = r.activity.mDecor;
        if (v != null) {
            if (r.activity.mVisibleFromServer) {
                mNumVisibleActivities--;
            }
            IBinder wtoken = v.getWindowToken();
            // Window 是否添加过,到WindowManager
            if (r.activity.mWindowAdded) {
                if (r.mPreserveWindow) {
                    r.mPendingRemoveWindow = r.window;
                    r.mPendingRemoveWindowManager = wm;
                    r.window.clearContentView();
                } else {
                    // 通知 WindowManager,移除当前 Window窗口
                    wm.removeViewImmediate(v);
                }
            }
} 

注意 performDestoryActivity() 将完成 Activity 生命周期 onDestory 方法回调。然后调用 WindowManager 的 removeViewImmediate():

/**
 * WindowManagerImpl
 */
@Override
public void removeViewImmediate(View view) {
    // 调用WindowManagerGlobal的removeView方法
    mGlobal.removeView(view, true);
}

即 AttachInfo 的释放操作是在 Activity 生命周期 onDestory 方法之后,在整个 Activity 的生命周期内都可以正常使用 View.post() 任务。

特别注意

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

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

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

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

总结

  1. 关于 View.post() 要注意在 API Level 24 前后的版本差异,不过该问题也不用过于担心,试想,会有哪些业务场景需要创建一个 View 却不把它添加到窗口视图呢?

  2. View.post() 任务能够保证在所有 View 绘制流程结束之后被调用,故如果需要依赖 View 绘制任务,此时可以优先考虑使用该机制。

View.post() 任务能够保证在所有 View 绘制流程结束之后被调用,故如果需要依赖 View 绘制任务,此时可以优先考虑使用该机制。


延伸

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

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值