从 Android 开发到读懂源码 第03期:View.post 源码解析

2022-04-25 08:46:07 浏览数 (1)

作者简介

罗铁锤,六年安卓踩坑经验,致力于底层平台、上层应用等多领域开发。文能静坐弹吉他,武能通宵写代码。

这个方法在日常开发中是经常用到的,例如在子线程中我们需要更新 UI,可以通过 post 一个 runnable ,在 run 方法中去绘制 UI ,或者我们需要在 Activity 的 onCreate 中获取一个 View 的宽高时,也会通过 post 一个 runnable 并在 run 方法中获取这个 View 的 width 和 height 信息。本文基于 Android 9.0 的源码进行分析。

1 post 方法的定义

首先,先从第一个问题开始分析,为什么我们在子线程调用 View 的 post 方法,在 run 里面就可以更新 UI 了呢?先看下这个 post 方法的定义:

代码语言:javascript复制
<View.java>
    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            // 主线程 handler 进行线程切换
            return attachInfo.mHandler.post(action);
        }

        // 将 runnable 临时存储到队列中
        // 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 是一个存储了 View 的信息,在整个 View 视图的树状结构中,都是共同引用同一个 AttachInfo 对象。这个在此暂不展开分析。

1.1 假设 attachInfo 不为空

则会直接调用 attachInfo 中的 mHandler 变量进行 post 操作,这里提前说明下:这个 mHandler 就是主线程的 Handler ,因此可以在 run 方法中直接进行 UI 操作了。

1.2 假设 attachInfo 为空

则进入下面的 getRunQueue().post(action) 。 getRunQueue 方法会返回一个 HandlerActionQueue 队列,这个队列从注释就能看出,只是作为一个临时缓存的容器,缓存的 runnable 会在 handler 和 view 关联的时机去执行。

代码语言:javascript复制
<View.java>
    // 获取 runnable 缓存队列
    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }

<HandlerActionQueue.java>
/**
 * Class used to enqueue pending work from Views when no Handler is attached.
 *
 * @hide Exposed for test framework only.
 */
public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;
    // 将 runnable 进行缓存,等待时机进行执行
    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  ;
        }
    }
    ...
    // 执行 runnable
    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;
        }
    }
    ...
    // 存储了 runnable 对象和 delay 延时
    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);
        }
    }
}

那这个 executeActions 方法具体是在什么时机执行的呢?一步步向上溯源,我们在 View 中找到这么一个方法:

代码语言:javascript复制
<View.java>
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        ...
        // Transfer all pending runnables.
        if (mRunQueue != null) {
            // 此处会将缓存的 runnable 交给 ViewRootImpl 的 mHandler 进行处理
            mRunQueue.executeActions(info.mHandler);
            // 移交后置空
            mRunQueue = null;
        }
        performCollectViewAttributes(mAttachInfo, visibility);
        // 熟悉的回调
        onAttachedToWindow();
        ...
    }

2 dispatchAttachedToWindow 流程

继续向上查找 dispatchAttachedToWindow ,发现 ViewGroup 中会调用该方法。我们都知道 ViewRootImpl 是整个视图结构的顶级管理,自然会想到是它在主动调用该方法。在 performTraversals 方法中,会调用 DecorView 的 dispatchAttachedToWindow 方法。此时调用(注意 mFirst 变量,此部分涉及 Window , Activity 及视图创建,不进行展开),说明 View 已经和 Window 进行关联, mAttachInfo 也已经实例化了,其内部的主线程 mHandler (主线程创建的)也实例化了,因此可以将 run 方法中的代码 post 到主线程执行,自然也就可以刷新 UI 了。

代码语言:javascript复制
<ViewRootImpl.java>
    private void performTraversals() {
        // cache mView since it is used so much below...
        ...

        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;

            final Configuration config = mContext.getResources().getConfiguration();
            if (shouldUseDisplaySize(lp)) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }

            // We used to use the following condition to choose 32 bits drawing caches:
            // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
            // However, windows are now always 32 bits by default, so choose 32 bits
            mAttachInfo.mUse32BitDrawingCache = true;
            mAttachInfo.mHasWindowFocus = false;
            mAttachInfo.mWindowVisibility = viewVisibility;
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mLastConfigurationFromResources.setTo(config);
            mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
            // Set the layout direction if it has not been set before (inherit is the default)
            if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
                host.setLayoutDirection(config.getLayoutDirection());
            }

            // ======>是在这里调用的,这个 host 是指向 mView 的引用,mView 本质上就是根布局 DecorView
            // 调用该方法实质上是将 view.post 的 runnable 移交给 mHandler,加入消息队列进行分发处理
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            dispatchApplyInsets(host);
        } else {
        ...

        // ======>View 的宽高测量
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        mIsInTraversal = false;
    }

回到问题 2,前面 View.post 到 ViewRootImpl.performTraversals 的流程都是一样的,但是有个问题, host.dispatchAttachedToWindow(mAttachInfo, 0); 这句代码执行是在 performMeasure 之前,按理来说应该此时应该也是获取不到宽高的。这里就涉及到 Handler 的消息队列,其消息队列是同步的(本场景),也就是说是阻塞的。我们先看下 performTraversals 是在哪里调用的:

代码语言:javascript复制
<ViewRootImpl.java>
    void doTraversal() {
        if (mTraversalScheduled) {
           ...
            // 此处调用
            performTraversals();
           ...
        }
    }

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            // 该 runnable 中 run 方法调用了 doTraversal
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 此处触发 mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

3 Choreographer#postCallback 触发绘制流程

往上查找,找到是 Choreographer#postCallback 触发了整个绘制流程, Choreographer 的 postCallback 方法中也是通过一个 mHandler 进行发送消息处理,这个 mHandler 是在其构造方法中传入了一个 Looper ,再看 Choreographer 实例化的时候发现传进来的 looper 正是主线程的 Looper 对象,说明该 mHandler 和 ViewRootImpl.mHandler 都是主线程 Handler 。因此在消息队列中, View.post 的消息是在 Choreographer.postCallback 消息之后,所以会先调用一次 performTraversals -> performMeasure 完成测量后,再处理 View.post 的消息,此刻 View 就能获取到宽高信息了。

代码语言:javascript复制
<Choreographer.java>
    // 也是通过 post 消息处理
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }

    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        ...
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }

    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        ...     // handler 发送消息处理
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
        ...
        }
    }
    // 这个也是主线程 handler
    private final FrameHandler mHandler;
    // 构造方法传入 looper 对象
    private Choreographer(Looper looper, int vsyncSource) {
        mLooper = looper;
        mHandler = new FrameHandler(looper);
        ...
    }

    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            // 对象是主线程实例化的,所以这个是主线程 looper
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };
    ...

    // Thread local storage for the SF choreographer.
    private static final ThreadLocal<Choreographer> sSfThreadInstance =
            new ThreadLocal<Choreographer>() {
                @Override
                protected Choreographer initialValue() {
                    // 对象是主线程实例化的,所以这个是主线程 looper
                    Looper looper = Looper.myLooper();
                    if (looper == null) {
                        throw new IllegalStateException("The current thread must have a looper!");
                    }
                    return new Choreographer(looper, VSYNC_SOURCE_SURFACE_FLINGER);
                }
            };

4 流程总结

这里再对 post 流程进行一次总结:

  • Choreographer.postCallback 触发 ViewRootImpl 的 TraversalRunnable.run 方法;
  • TraversalRunnable 的 run 方法中执行 doTraversal -> scheduleTraversals;
  • scheduleTraversals 中执行 DecorView 的 dispatchAttachedToWindow ,将 View.post 的 runnable 拿过来给 mHandler 进行处理(加入到主线程消息队列);
  • scheduleTraversals 继续往下执行 performMeasure ,完成宽高测量;
  • mHandler 取出后续消息,处理 View.post 中的 runnable ,在此刻 View 的宽高已经赋值。

专栏《从 Android 开发到读懂源码》系列文章推荐

第01期:requestFocus 源码分析

第02期:NestScroll 机制源码解析

0 人点赞