Android View#post源码分析

这个方法在日常开发中是经常用到的,例如在子线程中我们需要更新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的宽高已经赋值,因此可以获取到宽高信息了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351