View.Post () 的身世大揭祕

天星技術團隊發表於2018-08-01

作者:Insane 時間: 2018.7.31

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繪製,但是隻有非同步訊息才能被執行
            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(監控器),同步的訊息可以執行, 並且我們知道了 ViewRootImp 的 Handler 就是主執行緒的 ,而我們一開始 post 進來的 Runnable 也是在主執行緒的,而主執行緒的 Handler 是同步的,就是執行完一個 Message 才回繼續往下執行。那麼 我們再次回到 performTraversals( )

// 往下看的時候就會發現,在下面 又會再一次執行 scheduleTraversals( ),也就是代表會再一次執行  performTraversals( ),
  if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
        } else {
            if (isViewVisible) {
                // Try again
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }
複製程式碼

再一次執行的時候,已經是在 performMeasure( )之後了,那當執行到 我們 Post 的時候 自然而然就能得到寬高了。

相關文章