美文网首页程序员
Android View#post源码分析

Android View#post源码分析

作者: seagazer | 来源:发表于2019-12-26 21:51 被阅读0次

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

    1. 首先,先从第一个问题开始分析,为什么我们在子线程调用Viewpost方法,在run里面就可以更新UI了呢?先看下这个post方法的定义:
    <View.java>
        public boolean post(Runnable action) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                // 这个mHandler是主线程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;
        }
    
    1. 这里面的AttachInfo是一个存储了View的信息,在整个View视图的树状结构中,都是共同引用同一个AttachInfo对象。这个在此暂不展开分析。

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

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

    <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);
            }
        }
    }
    
    1. 那这个executeActions方法具体是在什么时机执行的呢?一步步向上溯源,我们在View中找到这么一个方法:
    <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();
            ...
        }
    
    1. 继续向上查找dispatchAttachedToWindow的调用地方,发现ViewGroup中重写方法中通过super调用该方法。
      分析过View的绘制流程的都知道ViewRootImpl是整个视图结构的顶级管理,自然会猜测是它在主动调用该方法。果然在performTraversals方法中,会调用DecorViewdispatchAttachedToWindow方法。此时调用,说明View已经和Window进行关联,mAttachInfo也已经实例化了,其内部的主线程mHandler(主线程创建的)也实例化了,因此可以将run方法中的代码post到主线程执行,自然也就可以刷新UI了。
    <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());
                }
    
                // 1======>是在这里调用的,这个host是指向mView的引用,mView本质上就是根布局DecorView
                // 调用该方法实质上是将view.post的runnable移交给mHandler,加入消息队列进行分发处理
                host.dispatchAttachedToWindow(mAttachInfo, 0);
                mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
                dispatchApplyInsets(host);
            } else {
            ...
    
            // 2======>View的宽高测量
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            mIsInTraversal = false;
        }
    
    1. 回到问题2,前面View.postViewRootImpl.performTraversals的流程都是一样的,但是有个问题,看上面代码的注释,host.dispatchAttachedToWindow(mAttachInfo, 0);这句代码执行是在performMeasure之前,按理来说应该此时应该也是获取不到宽高的。
      这里就涉及到Handler的消息队列,其消息队列是同步的(本场景),也就是说是阻塞的。我们先看下performTraversals是在哪里调用的:
    <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();
            }
        }
    
    1. 往上查找,找到是Choreographer#postCallback触发了整个绘制流程,ChoreographerpostCallback方法中也是通过一个FrameHandler进行发送消息处理。这个FrameHandler实例化时在其构造方法中传入了一个Looper
      再看看Choreographer的实例化,发现传进来的looper正是主线程的Looper对象,说明Choreographer中的mHandlerViewRootImpl.mHandler都是主线程Handler。因此在消息队列中,View.post的消息是在Choreographer.postCallback消息之后,所以会先调用一次performTraversals -> performMeasure完成测量后,再处理View.post的消息,此刻View就能获取到宽高信息了。
    <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);
                    }
                };
    
    1. 总结一下整个流程:
    • Choreographer.postCallback触发ViewRootImplTraversalRunnable.run方法
    • TraversalRunnable在其run方法中执行doTraversal -> scheduleTraversals
    • scheduleTraversals中执行DecorViewdispatchAttachedToWindow,将View.postrunnable拿过来给mHandler进行处理(加入到主线程消息队列)
    • scheduleTraversals继续往下执行performMeasure,完成宽高测量
    • mHandler取出后续消息,处理View.post中的runnable,在此刻View的宽高已经赋值,因此可以获取到宽高信息了。

    相关文章

      网友评论

        本文标题:Android View#post源码分析

        本文链接:https://www.haomeiwen.com/subject/ktfloctx.html