Android 之你真的了解 View.post() 原理嗎?

UI 優化系列專題,來聊一聊 Android 渲染相關知識,主要涉及 UI 渲染背景知識如何優化 UI 渲染兩部分內容。


UI 優化系列專題
  • UI 渲染背景知識

View 繪制流程之 setContentView() 到底做了什么?
View 繪制流程之 DecorView 添加至窗口的過程
深入 Activity 三部曲(3)View 繪制流程
Android 之 LayoutInflater 全面解析
關于渲染,你需要了解什么?
Android 之 Choreographer 詳細分析

  • 如何優化 UI 渲染

Android 之如何優化 UI 渲染(上)
Android 之如何優化 UI 渲染(下)


關于 View.post() 相信每個 Android 開發人員都不會感到陌生,它最常見的場景主要有兩種。

  1. 更新 UI 操作

  2. 獲取 View 的實際寬高

view.post() 的內部也是調用了 Handler,這可能是絕大多數開發人員所了解的,從本質來說這樣理解并沒有錯,不過它并能解釋上面提出的第 2 個場景。

在 Activity 中,View 繪制流程的開始時機是在 ActivityThread 的 handleResumeActivity 方法,在該方法首先完成 Activity 生命周期 onResume 方法回調,然后開始 View 繪制任務。也就是說 View 繪制流程要在 onResume 方法之后,但是我們絕大部分業務是在 onCreate 方法,比如要獲取某個 View 的實際寬高,由于 View 的繪制任務還未開始,所以就無法正確獲取。具體可以參考《View 繪制流程之 setContentView() 到底做了什么 ?

此時大家肯定使用過 View.post() 來解決該問題,注意 View 繪制流程也是向 Handler 添加任務,如果在 onCreate 方法直接使用 Handler.post(),則該任務一定在 View 繪制任務之前(同一個線程隊列機制)。

  • 注意這里不考慮使用 ViewTreeObserver 或更長延遲的 postDelayed()。

那 View.post() 內部也是使用 Handler,它是如何實現的呢?簡單來說,View.post() 對任務的運行時機做了調整。


View.post()

翻開 View 源碼,找到 View 的 post 方法如下:

public boolean post(Runnable action) {
    // 首先判斷AttachInfo是否為null
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果不為null,直接調用其內部Handler的post
        return attachInfo.mHandler.post(action);
    }

    // 否則加入當前View的等待隊列
    getRunQueue().post(action);
    return true;
}

注意 AttachInfo 是 View 的靜態內部類,每個 View 都會持有一個 AttachInfo,它默認為 null;需要先來看下 getRunQueue().post():

private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

getRunQueue() 返回的是 HandlerActionQueue,也就是調用了 HandlerActionQueue 的 post 方法:

public void post(Runnable action) {
    // 調用到postDelayed方法,這有點類似于Handler發送消息
    postDelayed(action, 0);
}

// 實際調用postDelayed
public void postDelayed(Runnable action, long delayMillis) {
    // HandlerAction表示要執行的任務
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            // 創建一個保存HandlerAction的數組
            mActions = new HandlerAction[4];
        }
        // 表示要執行的任務HandlerAction 保存在 mActions 數組中
        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        // mActions數組下標位置累加1
        mCount++;
    }
}

HandlerAction 表示一個待執行的任務,內部持有要執行的 Runnable 和延遲時間;類聲明如下:

private static class HandlerAction {
    // post的任務
    final Runnable action;
    // 延遲時間
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }

    // 比較是否是同一個任務
    // 用于匹配某個 Runnable 和對應的HandlerAction
    public boolean matches(Runnable otherAction) {
        return otherAction == null && action == null
                || action != null && action.equals(otherAction);
    }
}

注意 postDelayed() 創建一個默認長度為 4 的 HandlerAction 數組,用于保存 post() 添加的任務;跟蹤到這,大家是否有這樣的疑惑:View.post() 添加的任務沒有被執行?

實際上,此時我們要回過頭來,重新看下 AttachInfo 的創建過程,先看下它的構造方法:

AttachInfo(IWindowSession session, IWindow window, Display display,
               ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
               Context context) {
        mSession = session;
        mWindow = window;
        mWindowToken = window.asBinder();
        mDisplay = display;
        // 持有當前ViewRootImpl
        mViewRootImpl = viewRootImpl;
        // 當前渲染線程Handler
        mHandler = handler;
        mRootCallbacks = effectPlayer;
        // 為其創建一個ViewTreeObserver
        mTreeObserver = new ViewTreeObserver(context);
    }

注意 AttachInfo 中持有當前線程的 Handler。翻閱 View 源碼,發現僅有兩處對 mAttachInfor 賦值操作,一處是為其賦值,另一處是將其置為 null。

  • mAttachInfo 賦值過程:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 給當前View賦值AttachInfo,此時所有的View共用同一個AttachInfo(同一個ViewRootImpl內)
    mAttachInfo = info;
    // View浮層,是在Android 4.3添加的
    if (mOverlay != null) {
        // 任何一個View都有一個ViewOverlay
        // ViewGroup的是ViewGroupOverlay
        // 它區別于直接在類似RelativeLaout/FrameLayout添加View,通過ViewOverlay添加的元素沒有任何事件
        // 此時主要分發給這些View浮層
        mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
    }
    mWindowAttachCount++;

     // ... 省略

    if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER) != 0) {
        mAttachInfo.mScrollContainers.add(this);
        mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
    }
    //  mRunQueue,就是在前面的 getRunQueue().post()
    // 實際類型是 HandlerActionQueue,內部保存了當前View.post的任務
    if (mRunQueue != null) {
        // 執行使用View.post的任務
        // 注意這里是post到渲染線程的Handler中
        mRunQueue.executeActions(info.mHandler);
        // 保存延遲任務的隊列被置為null,因為此時所有的View共用AttachInfo
        mRunQueue = null;
    }
    performCollectViewAttributes(mAttachInfo, visibility);
    // 回調View的onAttachedToWindow方法
    // 在Activity的onResume方法中調用,但是在View繪制流程之前
    onAttachedToWindow();

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知所有監聽View已經onAttachToWindow的客戶端,即view.addOnAttachStateChangeListener();
            // 但此時View還沒有開始繪制,不能正確獲取測量大小或View實際大小
            listener.onViewAttachedToWindow(this);
        }
    }

    // ...  省略

    // 回調View的onVisibilityChanged
    // 注意這時候View繪制流程還未真正開始
    onVisibilityChanged(this, visibility);

    // ... 省略
}

方法最開始為當前 View 賦值 AttachInfo。注意 mRunQueue 就是保存了 View.post() 任務的 HandlerActionQueue;此時調用它的 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中,等待執行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        //此時不在需要,后續的post,將被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}

遍歷所有已保存的任務,發送到 Handler 中排隊執行;將保存任務的 mActions 置為 null,因為后續 View.post() 直接添加到 AttachInfo 內部的 Handler 。所以不得不去跟蹤 dispatchAttachedToWindow() 的調用時機。

ViewRootImpl

同一個 View Hierachy 樹結構中所有 View 共用一個 AttachInfo,AttachInfo 的創建是在 ViewRootImpl 的構造方法中:

mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
  • 一般 Activity 包含多個 View 形成 View Hierachy 的樹形結構,只有最頂層的 DecorView 才是對 WindowManagerService “可見的”。

dispatchAttachedToWindow() 的調用時機是在 View 繪制流程的開始階段。在 ViewRootImpl 的 performTraversals 方法,在該方法將會依次完成 View 繪制流程的三大階段:測量、布局和繪制,不過這部分不是今天要分析的重點。

// View 繪制流程開始在 ViewRootImpl
private void performTraversals() {
    // mView是DecorView
    final View host = mView;
    if (mFirst) {
        .....
        // host為DecorView
        // 調用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 給子view
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
        .....
    } 
   mFirst=false
   ...
   // Execute enqueued actions on every traversal in case a detached view   enqueued an action
   getRunQueue().executeActions(mAttachInfo.mHandler);
   // View 繪制流程的測量階段
   performMeasure();
   // View 繪制流程的布局階段
   performLayout();
   // View 繪制流程的繪制階段
   performDraw();
   ...

}

host 的實際類型是 DecorView,DecorView 繼承自 FrameLayout。

  • 每個 Activity 都有一個關聯的 Window 對象,用來描述應用程序窗口,每個窗口內部又包含一個 DecorView 對象,DecorView 對象用來描述窗口的視圖 — xml 布局。通過 setContentView() 設置的 View 布局最終添加到 DecorView 的 content 容器中。

跟蹤 DecorView 的 dispatchAttachedToWindow 方法的執行過程,DecorView 并沒有重寫該方法,而是在其父類 ViewGroup 中:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    // 子View的數量
    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍歷所有子View
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        // 遍歷調用所有子View的dispatchAttachedToWindow
        // 為每個子View關聯AttachInfo
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    // ...
}

for 循環遍歷當前 ViewGroup 的所有 childView,為其關聯 AttachInfo。子 View 的 dispatchAttachedToWindow 方法在前面我們已經分析過了:首先為當前 View 關聯 AttachInfo,然后將之前 View.post() 保存的任務添加到 AttachInfo 內部的 Handler

注意回到 ViewRootImpl 的 performTraversals 方法,咋一看,這個過程好像沒有太多新奇的地方。不過你是否注意到這一過程是在 View 的繪制任務中。

通過 View.post() 添加的任務,是在 View 繪制流程的開始階段,將所有任務重新發送到消息隊列的尾部,此時相關任務的執行已經在 View 繪制任務之后,即 View 繪制流程已經結束,此時便可以正確獲取到 View 的寬高了

View.post() 添加的任務能夠保證在所有 View(同一個 View Hierachy 內) 繪制流程結束之后才被執行

碎片化問題來了,如果我們只是創建一個 View,調用它的 post 方法,它會不會被執行呢?代碼如下:

final ImageView view = new ImageView(this);
    view.post(new Runnable() {
        @Override
        public void run() {
            // do something
        }
    });

答案是否定的,因為它沒有添加到窗口視圖,不會走繪制流程,自然也就不會被執行。此時只需要添加如下代碼即可:

// 將View添加到窗口
// 此時重新發起繪制流程,post任務會被執行
contentView.addView(view);

不過該問題在 API Level 24 之前不會發生,看下之前的代碼實現:

// API Level 24之前的post實現
public boolean post(Runnable action) {
    // 這里的邏輯與API Level 24及以后一致
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // 主要是這里,此時管理待執行的任務直接交給了 ViewRootImpl 中。
    // 而在API Level 24及以后,每個View自行維護待執行任務隊列,
    // 故,如果View不添加到Window視圖,dispatchAttachedToWindow 不會被調用,
    // View中的post任務將永遠得不到執行
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

在 API Level 24 之前,通過 View.post() 任務被直接添加到 ViewRootImpl 中,在 24 及以后,每個 View 自行維護待執行的 post() 任務,它們要依賴于 dispatchAttachedToWindow 方法,如果 View 未添加到窗口視圖,post() 添加的任務將永遠得不到執行

這樣的碎片化問題在 Android 中可能數不勝數,這也告誡我們如果對某項功能點了解的不夠充分,最后可能導致程序未按照意愿執行。

至此,View.post() 的原理我們就算搞清楚了,不過還是有必要跟蹤下 AttachInfo 的釋放過程。

  • mAttachInfo 置 null 的過程:

先看下表示 DecorView 的 dispatchDetachedFromWindow 方法,實際是調用其父類 ViewGroup 中:

// ViewGroup 的 dispatchDetachedFromWindow
void dispatchDetachedFromWindow() {

    // ... 省略

    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍歷所有childView
    for (int i = 0; i < count; i++) {
        // 通知childView dispatchDetachedFromWindow
        children[i].dispatchDetachedFromWindow();
    }

    // ... 省略

    super.dispatchDetachedFromWindow();
}

不出所料 ViewGroup 的 dispatchDetachedFromWindow 方法會遍歷所有 childView。

void dispatchDetachedFromWindow() {
    AttachInfo info = mAttachInfo;
    if (info != null) {
        int vis = info.mWindowVisibility;
        if (vis != GONE) {
            // 通知 Window顯示狀態發生變化
            onWindowVisibilityChanged(GONE);
            if (isShown()) {
                onVisibilityAggregated(false);
            }
        }
    }
    // 回調View的onDetachedFromWindow
    onDetachedFromWindow();
    onDetachedFromWindowInternal();

    // ... 省略

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        // 通知所有監聽View已經onAttachToWindow的客戶端,即view.addOnAttachStateChangeListener();
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知回調 onViewDetachedFromWindow
            listener.onViewDetachedFromWindow(this);
        }
    }

    // ... 省略

    // 將AttachInfo置為null
    mAttachInfo = null;
    if (mOverlay != null) {
        // 通知浮層View
        mOverlay.getOverlayView().dispatchDetachedFromWindow();
    }

    notifyEnterOrExitForAutoFillIfNeeded(false);
}

可以看到在 dispatchDetachedFromWindow 方法,首先回調 View 的 onDetachedFromWindow(),然后通知所有監聽者 onViewDetachedFromWindow(),最后將 mAttachInfo 置為 null。

由于 dispatchAttachedToWindow 方法是在 ViewRootImpl 中完成,此時很容易想到它的釋放過程肯定也在 ViewRootImpl,跟蹤發現如下調用過程:

void doDie() {
    // 檢查執行線程
    checkThread();

    synchronized (this) {
        if (mRemoved) {
            return;
        }
        mRemoved = true;
        if (mAdded) {
            // 回調View的dispatchDetachedFromWindow
            dispatchDetachedFromWindow();
        }

        if (mAdded && !mFirst) {
            destroyHardwareRenderer();

            // mView是DecorView
            if (mView != null) {
                int viewVisibility = mView.getVisibility();
                // 窗口狀態是否發生變化
                boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                if (mWindowAttributesChanged || viewVisibilityChanged) {
                    try {
                        if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                            mWindowSession.finishDrawing(mWindow);
                        }
                    } catch (RemoteException e) {
                    }
                }
                // 釋放畫布
                mSurface.release();
            }
        }

        mAdded = false;
    }

    // 將其從WindowManagerGlobal中移除
    // 移除DecorView
    // 移除DecorView對應的ViewRootImpl
    // 移除DecorView
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

可以看到 dispatchDetachedFromWindow 方法被調用,注意方法最后將 ViewRootImpl 從 WindowManager 中移除。

經過前面的分析我們已經知道 AttachInfo 的賦值操作是在 View 繪制任務的開始階段,而它的調用者是 ActivityThread 的 handleResumeActivity 方法,即 Activity 生命周期 onResume 方法之后。

那它是在 Activity 的哪個生命周期階段被釋放的呢?在 Android 中, Window 是 View 的容器,而 WindowManager 則負責管理這些窗口,具體可以參考《View 繪制流程之 DecorView 添加至窗口的過程》。

我們直接找到管理應用進程窗口的 WindowManagerGlobal,查看 DecorView 的移除工作:

/**
 * 將DecorView從WindowManager中移除
 */
public void removeView(View view, boolean immediate) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }

    synchronized (mLock) {
        // 找到保存該DecorView的下標,true表示找不到要拋出異常
        int index = findViewLocked(view, true);
        // 找到對應的ViewRootImpl,內部的DecorView
        View curView = mRoots.get(index).getView();
        // 從WindowManager中移除該DecorView
        // immediate 表示是否立即移除
        removeViewLocked(index, immediate);
        if (curView == view) {
            // 判斷要移除的與WindowManager中保存的是否為同一個
            return;
        }

        // 如果不是同一個View(DecorView),拋異常
        throw new IllegalStateException("Calling with view " + view
                + " but the ViewAncestor is attached to " + curView);
    }
}

根據要移除的 DecorView 找到在 WindowManager 中保存的 ViewRootImpl,真正移除是在 removeViewLocked 方法:

private void removeViewLocked(int index, boolean immediate) {
    // 找到對應的ViewRootImpl
    ViewRootImpl root = mRoots.get(index);
    // 該View是DecorView
    View view = root.getView();

    // ... 省略
    
    // 調用ViewRootImpl的die
    // 并且將當前ViewRootImpl在WindowManagerGlobal中移除
    boolean deferred = root.die(immediate);
    if (view != null) {
        // 斷開DecorView與ViewRootImpl的關聯
        view.assignParent(null);
        if (deferred) {
            // 返回 true 表示延遲移除,加入待死亡隊列
            mDyingViews.add(view);
        }
    }
}

可以看到調用了 ViewRootImpl 的 die 方法,回到 ViewRootImpl 中:

boolean die(boolean immediate) {
    // immediate 表示立即執行
    // mIsInTraversal 表示是否正在執行繪制任務
    if (immediate && !mIsInTraversal) {
        // 內部調用了View的dispatchDetachedFromWindow
        doDie();
        // return false 表示已經執行完成
        return false;
    }

    if (!mIsDrawing) {
        // 釋放硬件加速繪制
        destroyHardwareRenderer();
    } 
    // 如果正在執行遍歷繪制任務,此時需要等待遍歷任務完成
    // 故發送消息到尾部
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

注意 doDie 方法(源碼在前面已經貼出),它最終會調用 dispatchDetachedFromWindow 方法。

最后,移除 Window 窗口任務是通過 ActivityThread 完成的,具體調用在 handleDestoryActivity 方法完成:

private void handleDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    // 回調 Activity 的 onDestory 方法
    ActivityClientRecord r = performDestroyActivity(token, finishing,
            configChanges, getNonConfigInstance);
    if (r != null) {
        cleanUpPendingRemoveWindows(r, finishing);

        // 獲取當前Window的WindowManager, 實際是WindowManagerImpl
        WindowManager wm = r.activity.getWindowManager();
        // 當前Window的DecorView
        View v = r.activity.mDecor;
        if (v != null) {
            if (r.activity.mVisibleFromServer) {
                mNumVisibleActivities--;
            }
            IBinder wtoken = v.getWindowToken();
            // Window 是否添加過,到WindowManager
            if (r.activity.mWindowAdded) {
                if (r.mPreserveWindow) {
                    r.mPendingRemoveWindow = r.window;
                    r.mPendingRemoveWindowManager = wm;
                    r.window.clearContentView();
                } else {
                    // 通知 WindowManager,移除當前 Window窗口
                    wm.removeViewImmediate(v);
                }
            }
} 

注意 performDestoryActivity() 將完成 Activity 生命周期 onDestory 方法回調。然后調用 WindowManager 的 removeViewImmediate():

/**
 * WindowManagerImpl
 */
@Override
public void removeViewImmediate(View view) {
    // 調用WindowManagerGlobal的removeView方法
    mGlobal.removeView(view, true);
}

即 AttachInfo 的釋放操作是在 Activity 生命周期 onDestory 方法之后,在整個 Activity 的生命周期內都可以正常使用 View.post() 任務。

總結
  1. 關于 View.post() 要注意在 API Level 24 前后的版本差異,不過該問題也不用過于擔心,試想,會有哪些業務場景需要創建一個 View 卻不把它添加到窗口視圖呢?

  2. View.post() 任務能夠保證在所有 View 繪制流程結束之后被調用,故如果需要依賴 View 繪制任務,此時可以優先考慮使用該機制。


最后,如果需要更好的理解 View.post() 執行原理,可能還需要進一步理解 AttachInfo 的創建過程,關于這部分的詳細分析,你可以參考《Android 之 ViewTreeObserver 全面解析》。

文中如有不妥或有更好的分析結果,歡迎您的分享留言或指正。

文章如果對你有幫助,請留個贊吧!


擴展閱讀

其他專題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容