View.Post () 的身世大揭秘

View.post( ),大家肯定都用过,也就不陌生了。一般使用View.Post ( ) 的场景最常见的就是
1.子线程更UI,
2.获取View的宽高

那就让我们再带着问题去看看原因咯。

    public boolean post(Runnable action) {
     //判断 attachInfo 是否为空,而进行不同的操作
     //那么其实就是要知道 mAttachInfo 是在哪里被赋值的?
        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;
    }
mAttachInfo的赋值

我们会发现他两个被赋值的地方,分别为 dispatchAttachedToWindowdispatchDetachedFromWindow

//dispatchAttachedToWindow:
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        //这里赋值
        mAttachInfo = info;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
        }
        mWindowAttachCount++;
        // We will need to evaluate the drawable state at least once.
        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
        if (mFloatingTreeObserver != null) {
            info.mTreeObserver.merge(mFloatingTreeObserver);
            mFloatingTreeObserver = null;
        }
       registerPendingFrameMetricsObservers();

        if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {
            mAttachInfo.mScrollContainers.add(this);
            mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
        }
        // Transfer all pending runnables.
        //缓存不为空的是时候去执行 缓存的 action
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        performCollectViewAttributes(mAttachInfo, visibility);
        //当对应的 Activity 被添加到 Window的时候调用,只调用一次
        onAttachedToWindow();
   //  .......省略代码
//dispatchDetachedFromWindow:
  void dispatchDetachedFromWindow() {
        AttachInfo info = mAttachInfo;
        if (info != null) {
            int vis = info.mWindowVisibility;
            if (vis != GONE) {
                onWindowVisibilityChanged(GONE);
                if (isShown()) {
                    // Invoking onVisibilityAggregated directly here since the subtree
                    // will also receive detached from window
                    onVisibilityAggregated(false);
                }
            }
        }

        onDetachedFromWindow();
        onDetachedFromWindowInternal();

        InputMethodManager imm = InputMethodManager.peekInstance();
        if (imm != null) {
            imm.onViewDetachedFromWindow(this);
        }

        ListenerInfo li = mListenerInfo;
        final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
                li != null ? li.mOnAttachStateChangeListeners : null;
        if (listeners != null && listeners.size() > 0) {
            // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
            // perform the dispatching. The iterator is a safe guard against listeners that
            // could mutate the list by calling the various add/remove methods. This prevents
            // the array from being modified while we iterate it.
            for (OnAttachStateChangeListener listener : listeners) {
                listener.onViewDetachedFromWindow(this);
            }
        }

        if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
            mAttachInfo.mScrollContainers.remove(this);
            mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;
        }
        //这里赋空值
        mAttachInfo = null;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchDetachedFromWindow();
        }

        notifyEnterOrExitForAutoFillIfNeeded(false);
    }

但是会发现,到这里的时候我们无法再追踪这两个方法在哪里被调用了,于是我们可以通过网上那些Android源码阅读的网站,或者自己有下载Android源码的来找一找看看 究竟在什么地方被调用的:
推荐一个:http://androidxref.com/
搜索 dispatchAttachedToWindow,可以发现如下:

7403980-e96a4387af6ee912.png

可以发现,在 ViewGroupViewRootImpl 均有被调用,那么我们就去看看。

ViewGroup
 @Override
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
       // super.dispatchAttachedToWindow(info, visibility); 这句话就是说明他执行了父类的方法
      //也就是我们一开始看到的 View 的dispatchAttachedToWindow()的方法。
        super.dispatchAttachedToWindow(info, visibility);
        mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
    //这里又会把 mAttachInfo 作为参数传递进去,分别让自己的子类去执行 dispatchAttachedToWindow () 方法,
    //让自己的子类 分别给 mAttachInfo  赋值。
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
        }
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        for (int i = 0; i < transientCount; ++i) {
            View view = mTransientViews.get(i);
            view.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, view.getVisibility()));
        }
    }
/*但是这样一来我们还是不知道 mAttachInfo  是在那里被赋值的发,只是知道 ViewGroup 会去执行 View 类和掉用子 View 的 dispatchAttachedToWindow () 方法。
方法。*/

继续看看 ViewGroup 里面还有什么地方调用了:
addViewInner()方法是 viewGroup addView( )内部都会调用的一个方法

private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {

        if (mTransition != null) {
            // Don't prevent other add transitions from completing, but cancel remove
            // transitions to let them complete the process before we add to the container
            mTransition.cancel(LayoutTransition.DISAPPEARING);
        }
         //判断View是否被添加
        if (child.getParent() != null) {
            throw new IllegalStateException("The specified child already has a parent. " +
                    "You must call removeView() on the child's parent first.");
        }

        if (mTransition != null) {
            mTransition.addChild(this, child);
        }

        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }

        if (preventRequestLayout) {
            child.mLayoutParams = params;
        } else {
            child.setLayoutParams(params);
        }

        if (index < 0) {
            index = mChildrenCount;
        }
        //添加到 ViewGroup
        addInArray(child, index);

        // tell our children
        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
            child.mParent = this;
        }

        final boolean childHasFocus = child.hasFocus();
        if (childHasFocus) {
            requestChildFocus(child, child.findFocus());
        }
       //这里判断 mAttachInfo 的对象是否为空,如果不为空就把 mAttachInfo 作为参数调用子类的 dispatchAttachedToWindow ( ),那么
      //还是回到了 View 的 dispatchAttachedToWindow (),我们还是不知道 mAttachInfo 再哪里给赋值的.
        AttachInfo ai = mAttachInfo;
        if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
            boolean lastKeepOn = ai.mKeepScreenOn;
            ai.mKeepScreenOn = false;
            child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
            if (ai.mKeepScreenOn) {
                needGlobalAttributesUpdate(true);
            }
            ai.mKeepScreenOn = lastKeepOn;
        }
       .......省略

既然ViewGroup没有,那么我们就去看看 ViewRootImpl。

ViewRootImpl

我们在 ViewRootImpl 的 performTraversals(),发现了dispatchAttachedToWindow()被调用,而 performTraversals() 作用就是遍历整个View树,并且按照要求进行measure,layout和draw流程。

 private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
  //判断是不是第一次
  if (mFirst) {
       .....
      //这里调用了 dispatchAttachedToWindow,并且把 mAttachInfo 给子view
       host.dispatchAttachedToWindow(mAttachInfo, 0);
       mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
       dispatchApplyInsets(host);
     //Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
      .....
} 
   mFirst=false
    ...
  // Execute enqueued actions on every traversal in case a detached view enqueued an action
    getRunQueue().executeActions(mAttachInfo.mHandler);
    ...
    performMeasure();
    ...
    performLayout();
    ...
    performDraw();
    ...

上面的代码,等下我们再回来看,我们先找找在 ViewRootImpl 里面 mAttachInfo 是在哪被赋值的

        ...
       final View.AttachInfo mAttachInfo;
        ...
      // mAttachInfo 就是在这里被赋值了,其中在多个参数之中,我们发现了 mHandler。
       mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);


    //继续看看 mHandler 是在哪被初始化的。
  final ViewRootHandler mHandler = new ViewRootHandler();
/*
通过这句代码我们就可以知道。这里 new 的时候是无参构造函数,那默认绑定的就是当前线程的 Looper,而这句 new 代码是在主线程中执行的,所以这个 Handler 绑定的也就是主线程的 Looper
*/
再结合:getRunQueue().executeActions(mAttachInfo.mHandler);
   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;
        }
    }
为什么能更新UI:

总结回顾一下:
我们知道了 mAttachInfo 是在 ViewRootImpl 初始化的,再结合刚说等下回去看的 performTraversals 的方法,可以知道ViewRootImpl 会调用子view的 dispatchAttachedToWindow。我们还可以知道为什么 View.post(Runnable),可以更新UI了,因为这些 Runnable 操作都通过 ViewRootImpl 的 mHandler 切到主线程来执行了。

为什么能获取宽高

那么我们再次回到 一开始的的地方,我们知道 View 里面的 mAttachInfo 是在 ViewdispatchAttachedToWindow 被赋值,那么 dispatchAttachedToWindow()是在什么时候执行的呢?我们上面分析的是在哪调用了他,和 mAttachInfo的初始化,细心的朋友,会发现在ViewdispatchAttachedToWindow()onAttachedToWindow();,那么我们就可简单写个测试。

  class TestView : TextView {
    constructor(context: Context) : super(context) {}

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

    override fun onAttachedToWindow() {
        Log.e("TAG---AttachedToWindow", "onAttachedToWindow");
        super.onAttachedToWindow();
    }
}
//MainActivity :
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        Log.e("TAG---没有Post", "mButton width : " + tv_test.getMeasuredWidth() + " - height : " + tv_test.getMeasuredHeight());
        tv_test.post {
            Log.e("TAG---Post", "mButton width : " + tv_test.getMeasuredWidth() + " - height : " + tv_test.getMeasuredHeight());
        }

    }
}

//结果:
07-29 17:17:24.201 31814-31814/? E/TAG---没有Post: mButton width : 0 - height : 0
07-29 17:17:24.261 31814-31814/? E/TAG---AttachedToWindow: onAttachedToWindow
07-29 17:17:24.351 31814-31814/? E/TAG---Post: mButton width : 84 - height : 57

那么结果就出来了,在 onCreate 中获取宽高,AttachedToWindow ( ) 是还没执行的,那就说明一开始的时候 mAttachInfo 是为空值的,那么我们再看开头的第一段代码:

    public boolean post(Runnable action) {
        // mAttachInfo 为空
        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;
    }
那么他就会执行: getRunQueue().post(action);
 public class HandlerActionQueue {
    private HandlerAction[] mActions;
    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];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }

我们post()传进来的 Runnable 会先经过 HandlerAction 包装一下,然后再缓存起来。HandlerActionQueue 是通过一个默认大小为4的数组保存这些 Runnable 操作的,如果数组不够时,就会通过 GrowingArrayUtils 来扩充数组。那么既然是被缓存起来的,那么他是什么时候执行呢?我又会发现 他执行的方法还是在 dispatchAttachedToWindow 里面:

dispatchAttachedToWindow :
     //缓存不为空的是时候去执行 缓存的 action
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }

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.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0;
        }
    }

既然我们知道了 post()传进来的 Runnable 会在 dispatchAttachedToWindow 执行,结合我们上面的分析,我们就可以知道,post 的操作是要经过 ViewRootImpl 的 performTraversals(),而它的作用就是遍历整个View树,并且按照要求进行measure,layout和draw流程。但是仔细看看代码,我们会发现:

 private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
  //判断是不是第一次
  if (mFirst) {
       .....
      //这里调用了 dispatchAttachedToWindow,明显是在 performMeasure 之前,
     //为什么在测量之前调用还能得到宽高呢?
       host.dispatchAttachedToWindow(mAttachInfo, 0);
       mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
       dispatchApplyInsets(host);
     //Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
      .....
} 
   mFirst=false
    ...
  // Execute enqueued actions on every traversal in case a detached view enqueued an action
    getRunQueue().executeActions(mAttachInfo.mHandler);
    ...
    performMeasure();
    ...
    performLayout();
    ...
    performDraw();
    ...

那么 为什么明明测量 performMeasure(); 的是在 dispatchAttachedToWindow 之后执行,但是我们却能得到测量后的宽高?请看下面的代码:

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
          // doTraversal 这里执行
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();


    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
          //向Looper中移除了Barrier(同步屏障),同步的消息可以执行
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            //performTraversals() 在这里被执行
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
mTraversalBarrier 是什么东东?

为了让View能够有快速的布局和绘制,android中定义了一个Barrier的概念,当View在绘制和布局时会向Looper中添加了Barrier(同步屏障),这样后续的消息队列中的同步的消息将不会被执行,以免会影响到UI绘制,但是只有异步消息才能被执行。 所谓的异步消息也只是体现在这,添加了Barrier后,消息还可以继续被执行,不会被推迟运行。 如何使用异步消息,只有在创建Handler(构造方法的参数上标识是否异步消息)的时候或者在发送Message(Mesasge#setAsynchronous(true))时进行设置。而异步消息应用层是无法设置,因为相关设置的方法均是Hide的。

那就是什么意思呢?
首先,我们搞清楚 mTraversalScheduled 这个对象是在哪被赋值。

 void scheduleTraversals() {
        if (!mTraversalScheduled) {
           //这里
            mTraversalScheduled = true;
           //向Looper中添加了Barrier(同步屏障),这样后续的消息队列中的同步的消息将不会被执行,
           //以免会影响到UI绘制,但是只有异步消息才能被执行
           //设置同步障碍,确保mTraversalRunnable优先被执行
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //这里又调用了  mTraversalRunnable,执行异步
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

    void unscheduleTraversals() {
        if (mTraversalScheduled) {
           //这里
            mTraversalScheduled = false;
           //向Looper中移除了Barrier(同步屏障),同步的消息可以执行
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        //这里又调用了  mTraversalRunnable。
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

scheduleTraversals ( ) 方法是在 requestLayout( )被调用的, requestLayout( )是什么?额,这里就不解释了,不然又要写一大堆,哈哈。只要知道 第一次调用requestLayout( ) 就是引起整个 View 的绘制流程

 @Override
 public void requestLayout() {
     if (!mHandlingLayoutInLayoutRequest) {
       // 检查当前线程
         checkThread();
         mLayoutRequested = true;
        // 调用绘制
         scheduleTraversals();

那么就是 scheduleTraversals( )——》TraversalRunnable ( )——》doTraversal( )——》performTraversals( ),而且 doTraversal ( )中向Looper中移除了Barrier(同步屏障),同步的消息可以执行,
经查阅资料发现,跟Android系统的消息机制有关系(猜测更上面的同步机制有关),performTraversals会先执行dispatchAttachedToWindow,这个时候会将任务post到主线程的MessageQueue等待执行,然后performTraversals方法会继续执行,完全执行完后,Looper再去消费下一个Message,这个时候才有可能会拿到post的Runnable,因此Runnabel操作实际是在performMeasure操作后才执行的,宽高自然就取到了。

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

推荐阅读更多精彩内容