Android 之 ViewTreeObserver 全面解析

UI 優(yōu)化系列專題惋砂,來聊一聊 Android 渲染相關(guān)知識鞠绰,主要涉及 UI 渲染背景知識如何優(yōu)化 UI 渲染兩部分內(nèi)容祠挫。


UI 優(yōu)化系列專題
  • UI 渲染背景知識

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

  • 如何優(yōu)化 UI 渲染

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


在 Android 中倦炒,如果想要獲取 View 的某些狀態(tài),大家肯定使用過 ViewTreeObserver软瞎,通過名字也可以得知它是 ViewTree 的觀察者逢唤,先看下它的自我介紹:

ViewTreeObserver 用于注冊可被全局通知的 Listener 在 ViewTree 狀態(tài)發(fā)生變化時。這些 Listeners 包括 View 開始繪制涤浇、窗口焦點發(fā)生變化等場景鳖藕。ViewTreeObserver 不能被外部實例化,只能通過 View 的 getViewTreeObserver() 方式獲取只锭。

  • ViewTree著恩,一般 Activity 包含多個 View 形成 View Hierachy 的樹形結(jié)構(gòu)

也就是 ViewTreeObserver 為應(yīng)用提供了全局監(jiān)聽的 View 狀態(tài)變化,那具體可以幫助我們監(jiān)聽哪些狀態(tài)呢蜻展?

序號 Linster 作用
1 OnWindowAttachListener 當(dāng)視圖層次結(jié)構(gòu)關(guān)聯(lián)到窗口或與之分離時回調(diào)
2 OnWindowFocusChangeListener 當(dāng)視圖層次結(jié)構(gòu)的窗口焦點狀態(tài)發(fā)生變化時回調(diào)
3 OnGlobalFocusChangeListener 當(dāng)視圖樹中的焦點狀態(tài)更改時回調(diào)
4 OnGlobalLayoutListener 當(dāng)全局布局狀態(tài)或視圖樹中視圖的可見性更改時回調(diào)
5 OnPreDrawListener 當(dāng)視圖即將繪制時回調(diào)
6 OnDrawListener 當(dāng)視圖樹即將繪制時
7 OnTouchModeChangeListener 當(dāng)觸摸模式改變時回調(diào)
8 OnScrollChangedListener 當(dāng)視圖樹中的某些內(nèi)容被滾動時回調(diào)
9 ... 被 @hide 聲明

Listeners 在 ViewTreeObserver 中的管理:

private CopyOnWriteArrayList<OnWindowAttachListener> mOnWindowAttachListeners;
private CopyOnWriteArrayList<OnWindowFocusChangeListener> mOnWindowFocusListeners;
private CopyOnWriteArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
private ArrayList<OnDrawListener> mOnDrawListeners;
private CopyOnWriteArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;

ViewTreeObserver 內(nèi)部使用 CopyOnWriteArrayList 保存相關(guān) Listener喉誊,這主要考慮可能出現(xiàn)的多線程場景,不過熟悉它的朋友知道纵顾,之所以叫做 CopyOnWrite 就是當(dāng)添加元素的時候伍茄,不直接向當(dāng)前容器添加,而是先將當(dāng)前容器進行Copy施逾,復(fù)制出一個新的容器敷矫,然后新的容器里添加元素,添加完元素之后汉额,再將原容器的引用指向新的容器曹仗。如果頻發(fā)的申請與釋放其實對內(nèi)存還是有一定影響的。

ViewTreeObserver 不能被外部實例化蠕搜,只能通過 view.getViewTreeObserver() 方式獲仍趺!:

 public ViewTreeObserver getViewTreeObserver() {
    if (mAttachInfo != null) {
        // 在繪制流程開始時,才會被賦值妓灌,
        // 此時所有的View共用ViewRootImpl中提供的ViewTreeObserver
        return mAttachInfo.mTreeObserver;
    }
    if (mFloatingTreeObserver == null) {
        // 懶加載遭居,為當(dāng)前View創(chuàng)建ViewTreeObserver
        mFloatingTreeObserver = new ViewTreeObserver(mContext);
    }
    return mFloatingTreeObserver;
}

注意 mAttachInfo啼器,它的類型是 AttachInfo,內(nèi)部持有一個 ViewTreeObserver俱萍,mAttachInfo 在 View 繪制任務(wù)的開始階段被賦值端壳,但是在此之前,每個 View 會臨時創(chuàng)建一個 ViewTreeObserver 用于保存當(dāng)前添加的 Listener枪蘑。View 一旦被關(guān)聯(lián) AttachInfo损谦,它會合并該 View 的 ViewTreeObserver,后續(xù)的添加的 Listener 都被保存在該 AttachInfo 內(nèi)部的 ViewTreeObserver岳颇,即同一個 View Hierarchy 內(nèi)所有 View 共用同一個 ViewTreeObserver照捡。該過程在后面會分析到。

獲取到 ViewTreeObserver 后就可以添加 Listener 了话侧,ViewTreeObserver 內(nèi)部對所有保存 Listener 的容器均采用懶加載方式管理栗精,這里僅以 OnWindowAttachListener 為例:

public void addOnWindowAttachListener(OnWindowAttachListener listener) {
    // 檢查當(dāng)前ViewTreeObserver是否可用
    // 當(dāng)繪制流程開始時,所有 View 內(nèi)部的ViewTreeObserver 替換為
    // ViewRootImpl 內(nèi)部的瞻鹏,故在同一個 View Hierarchy 內(nèi)共用同一個悲立。
    checkIsAlive();

    if (mOnWindowAttachListeners == null) {
       // 當(dāng)使用時在創(chuàng)建
        mOnWindowAttachListeners
                = new CopyOnWriteArrayList<OnWindowAttachListener>();
    }

    mOnWindowAttachListeners.add(listener);
}

具體的 Listener 是在什么時機被回調(diào)的呢?既然所有 View(同一個 View Hierarchy 內(nèi)) 的 ViewTreeObserver 最終要被合并到 AttachInfo 內(nèi)的 ViewTreeObserver新博,在合并之前肯定不會被調(diào)用薪夕,所以,此時有必要去跟蹤下 AttachInfo 被關(guān)聯(lián)的時機:

 /**執(zhí)行View繪制任務(wù)*/
private void performTraversals() {

   // ... 省略

   // host是DecorView
   // 每個ViewRootImpl包含一個AttachInfo
   // 為所有的childView關(guān)聯(lián)AttachInfo赫悄,即同一個View Hierarchy內(nèi)共用一個AttachInfo  
   // AttachInfo 中包含ViewTreeObserver
   host.dispatchAttachedToWindow(mAttachInfo, 0);
   // 回調(diào) OnWindowAttachedListener
   mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);

   ...

   // 開始測量
   performMeasure();
   ...

   // 開始布局
   performLayout();

   ...

   // 開始繪制
   performDraw();

    ...
}

在 Activity 中原献,View 繪制任務(wù)的開始時機是在 ActivityThread 的 handleResumeActivity 方法,在該方法首先完成 onResume 生命周期方法回調(diào)埂淮。然后發(fā)起 View 繪制任務(wù)姑隅,最終會執(zhí)行到 ViewRootImpl 的 performTraversals 方法,在該方法真正開始執(zhí)行 View 的三大繪制流程倔撞。具體你可以參考《深入 Activity 三部曲(3)之 View 繪制流程

不過粤策,今天我們要關(guān)注的是 ViewTreeObserver,注意上面的 host误窖,它的實際類型是 DecorView。具體可以參考這里《View 繪制流程之 setContentView() 到底做了什么 秩贰?

  • 每個 Activity 都有一個關(guān)聯(lián)的 Window 對象(實際類型是 PhoneWindow)霹俺,用來描述應(yīng)用程序窗口,每個窗口內(nèi)部又包含一個 DecorView 對象(繼承自 FrameLayout)毒费,DecorView 用來描述窗口的根視圖 — xml 布局丙唧。

這里實際調(diào)用其父類 ViewGroup 的 dispatchAttachedToWindow 方法,參數(shù) mAttachInfo 很關(guān)鍵觅玻,看下它在 ViewRootImpl 中的聲明:

// AttachInfo 內(nèi)部包含一個ViewTreeObserver
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);

ViewGroup 的 dispatchAttachedToWindow 方法如下:

// ViewGroup重寫View的dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍歷所有childView
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        // 調(diào)用子View的dispatchAttachedToWindow
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
}

可以看到遍歷所有 childView想际,將 ViewRootImpl 的 AttachInfo 作為參數(shù)調(diào)用子 View 的 dispatchAttachedToWindow 方法:

// View的dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 給當(dāng)前View賦值A(chǔ)ttachInfo培漏,此時所有的View共用同一個AttachInfo(同一個ViewRootImpl內(nèi))
    mAttachInfo = info;
    // View浮層,是在Android 4.3添加的
    if (mOverlay != null) {
        // 任何一個View都有一個ViewOverlay
        // ViewGroup的是ViewGroupOverlay
        // 它區(qū)別于直接在類似RelativeLaout/FrameLayout添加View胡本,通過ViewOverlay添加的元素沒有任何事件
        // 此時主要分發(fā)給這些View浮層
        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) {
        // 合并當(dāng)前View的ViewTreeObserver到ViewRootImpl的ViewTreeObserver
        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.
    if (mRunQueue != null) {
        // 執(zhí)行使用View.post的任務(wù)牌柄,info是AttachInfo
        mRunQueue.executeActions(info.mHandler);
        // 保存延遲任務(wù)的隊列被置為null,因為此時所有的View共用AttachInfo
        mRunQueue = null;
    }
    performCollectViewAttributes(mAttachInfo, visibility);
    // 回調(diào)View的onAttachedToWindow方法
    // 在Activity的onResume方法中調(diào)用侧甫,但是在View繪制流程之前
    onAttachedToWindow();

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

    int vis = info.mWindowVisibility;
    if (vis != GONE) {
        onWindowVisibilityChanged(vis);
        if (isShown()) {
            // Calling onVisibilityAggregated directly here since the subtree will also
            // receive dispatchAttachedToWindow and this same call
            onVisibilityAggregated(vis == VISIBLE);
        }
    }

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

    if ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) != 0) {
        // If nobody has evaluated the drawable state yet, then do it now.
        refreshDrawableState();
    }
    needGlobalAttributesUpdate(false);

    notifyEnterOrExitForAutoFillIfNeeded(true);
}

在該方法披粟,首先將 AttachInfo 賦值給當(dāng)前 childView咒锻,記得前面分析到的 addXxxListener(),此后所有添加的 Listener 都被保存在該 AttachInfo 的 ViewTreeObserver 內(nèi)守屉。

這里還要著重看下 info.mTreeObserver.merge()惑艇,前面有提到,View 在被關(guān)聯(lián) AttachInfo 之前拇泛,每個 View 擁有自己臨時的 ViewTreeObserver滨巴,在繪制任務(wù)的開始階段,會為每個 View 關(guān)聯(lián)一個 AttachInfo碰镜,此時便將當(dāng)前 View 的 ViewTreeObserver 合并到 AttachInfo 的 ViewTreeObserver兢卵。 看下這一合并過程 ViewTreeObserver 的 merge 方法:

/**
 * 在 ViewRootImpl 的 dispatchAttachedToWindow 方法被調(diào)用
 * @param observer 子View的ViewTreeObserver
 */
void merge(ViewTreeObserver observer) {
    // 合并OnWindowAttachListener
    if (observer.mOnWindowAttachListeners != null) {
        if (mOnWindowAttachListeners != null) {
            // 添加到AttachInfo的ViewTreeObserver
            mOnWindowAttachListeners.addAll(observer.mOnWindowAttachListeners);
        } else {
            // 首次直接賦值
            mOnWindowAttachListeners = observer.mOnWindowAttachListeners;
        }
    }
    // 合并OnWindowFocusListener
    if (observer.mOnWindowFocusListeners != null) {
        if (mOnWindowFocusListeners != null) {
            mOnWindowFocusListeners.addAll(observer.mOnWindowFocusListeners);
        } else {
            mOnWindowFocusListeners = observer.mOnWindowFocusListeners;
        }
    }
    // 合并OnGlobalFocusListener
    if (observer.mOnGlobalFocusListeners != null) {
        if (mOnGlobalFocusListeners != null) {
            mOnGlobalFocusListeners.addAll(observer.mOnGlobalFocusListeners);
        } else {
            mOnGlobalFocusListeners = observer.mOnGlobalFocusListeners;
        }
    }

    // ... 省略

}

可以看到,依次將 View 內(nèi)部的 ViewTreeObserver 合并到 AttachInfo 的 ViewTreeObserver绪颖。

至此秽荤,ViewTreeObserver 的保存和管理機制就算是搞清楚了,那它們分別在什么時候被回調(diào)通知呢柠横?


1. OnWindowAttachListener

注意上面貼出的 performTraversals 方法窃款,該方法將依次完成 View 的三大繪制流程,不過在此之前先回調(diào) AttachInfo 內(nèi) ViewTreeObserver 的 dispatchOnWindowAttachChange 方法:

/*
 * 通知已注冊的監(jiān)聽器窗口已被附加/分離牍氛。
 */
final void dispatchOnWindowAttachedChange(boolean attached) {
    //注意:由于使用了CopyOnWriteArrayList晨继,必須使用迭代器來
    //執(zhí)行調(diào)度。迭代器是對偵聽器的安全防護
    //可以通過調(diào)用各種添加/刪除方法來改變列表搬俊。這樣可以防止
    //當(dāng)我們迭代數(shù)組時紊扬,數(shù)組不會被修改。
    final CopyOnWriteArrayList<OnWindowAttachListener> listeners
            = mOnWindowAttachListeners;
    if (listeners != null && listeners.size() > 0) {
        for (OnWindowAttachListener listener : listeners) {
            if (attached) {
                // View 被附加到窗口
                listener.onWindowAttached();
            } else {
                // View 從窗口中分離
                listener.onWindowDetached();
            }
        }
    }
}

即 onWindowAttached 方法的調(diào)用在 Activity 生命周期 onResume 方法之后唉擂,在繪制流程之前餐屎。

  • 此時 View 的繪制流程還未開始,在該方法不能直接獲取到 View 的實際寬高
 view.getViewTreeObserver().addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() {
         @Override
         public void onWindowAttached() {
             // 這里不能正確獲取到 View 的寬高
             int width = view.getWidth();
             int height = view.getHeight();
         }

         @Override
         public void onWindowDetached() {
             // onWindowDetached 的回調(diào)時機又在什么時候呢玩祟?
         }
  });

那 onWindowDetached 方法是在什么時候被回調(diào)呢腹缩?又在 Activity 的哪個生命周期階段呢?

它的調(diào)用時機是在 ActivityThread 的 handleDestoryActivity 方法,在該方法首先回調(diào) Activity 的 onDestory 生命周期方法藏鹊,然后將當(dāng)前窗口從 WindowManager 移除润讥,最終調(diào)用到 ViewRootImpl 的 doDie(),在該方法將完成 dispatchDetachedFromWindow 通知盘寡。感興趣的朋友可以跟蹤下這一過程或參考這里楚殿。

void dispatchDetachedFromWindow() {
    if (mView != null && mView.mAttachInfo != null) {
        // 回調(diào) OnWindowAttachListener 的 onWindowDetached 方法
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
        mView.dispatchDetachedFromWindow();
    }
    // ... 省略 
}
  • 即 onWindowAttached() 在 Activity 生命周期 onResume 方法之后,但是在 View 繪制流程之前宴抚;onWindowDetached() 在 Activity 生命周期 onDestory 方法之后勒魔,此時 View 已經(jīng)從窗口中移除。
2. OnWindowFocusChangeListener

OnWindowFocusChangeListener 表示當(dāng)窗口焦點發(fā)生變化時回調(diào)菇曲,看下它在 ViewTreeObserver 內(nèi)的分發(fā)過程:

    /**
     * 通知已注冊的偵聽器窗口焦點已更改冠绢。
     */
    final void dispatchOnWindowFocusChange(boolean hasFocus) {
        final CopyOnWriteArrayList<OnWindowFocusChangeListener> listeners
                = mOnWindowFocusListeners;
        if (listeners != null && listeners.size() > 0) {
            for (OnWindowFocusChangeListener listener : listeners) {
                // 回調(diào)OnWindowFocusChangeListener的onWindowFocusChanged方法
                listener.onWindowFocusChanged(hasFocus);
            }
        }
    }

它是在哪里被回調(diào)的呢?猜測肯定還是離不開 ViewRootImpl常潮。查找發(fā)現(xiàn)在 ViewRootImpl 的靜態(tài)內(nèi)部類 W 中弟胀,簡單看下這一過程:

/**
 * mWindow是ViewRootImpl的靜態(tài)內(nèi)部類
 * 內(nèi)部持有當(dāng)前ViewRootImpl
 */
static class W extends IWindow.Stub {
    //弱引用持有當(dāng)前ViewRootImpl
    //因為它將被保存在WindowManagerService
    private final WeakReference<ViewRootImpl> mViewAncestor;
    private final IWindowSession mWindowSession;

    W(ViewRootImpl viewAncestor) {
        mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
        mWindowSession = viewAncestor.mWindowSession;
    }
    
    /**
     * 由遠程 WMS 通知窗口焦點發(fā)生變化
     */
    public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
       final ViewRootImpl viewAncestor = mViewAncestor.get();
       if (viewAncestor != null) {
           // ViewRootImpl的windowFocusChanged方法
           viewAncestor.windowFocusChanged(hasFocus, inTouchMode);
       }
    }

      // ... 省略
}

W 本質(zhì)是一個 Binder 對象,內(nèi)部通過弱引用持有當(dāng)前的 ViewRootImpl喊式,它將作為客戶端保存在 WMS(WindowManagerService)孵户,用來和 WindowSession 交互,實際間接在與 WMS 交互岔留。

windowFocusChanged 方法最終會發(fā)送消息到當(dāng)前繪制線程夏哭,關(guān)鍵代碼如下:

case MSG_WINDOW_FOCUS_CHANGED: {
    if(mView != null){
       // 通知 View 窗口焦點發(fā)生變化
       mView.dispatchWindowFocusChanged(hasWindowFocus);
       // 通知 ViewTreeObserver 窗口焦點發(fā)生變化
       mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus);
    }
}
3. OnGlobalFocusChangeListener

OnGlobalFocusChangeListener 表示當(dāng)視圖樹中的焦點狀態(tài)更改時回調(diào),它在 ViewTreeObserver 內(nèi)的分發(fā)機制如下:

/**
 * 通知已注冊的偵聽器焦點已更改献联。
 */
final void dispatchOnGlobalFocusChange(View oldFocus, View newFocus) {
    final CopyOnWriteArrayList<OnGlobalFocusChangeListener> listeners = mOnGlobalFocusListeners;
    if (listeners != null && listeners.size() > 0) {
        for (OnGlobalFocusChangeListener listener : listeners) {
            // 回調(diào) onGlobalFocusChanged 方法
            listener.onGlobalFocusChanged(oldFocus, newFocus);
        }
    }
}
Parameters 說明
oldFocus 之前獲取到焦點的視圖
newFocus 當(dāng)前獲取到焦點的視圖
  • 注意 oldFocus 和 newFocus 都有可能為 null竖配。

如果要監(jiān)聽某個 View 是否獲取到焦點,我們可以使用如下方式:

mView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
      @Override
      public void onFocusChange(View v, boolean hasFocus) {
          // 監(jiān)聽當(dāng)前 View 是否獲取到焦點
      }
});

不過里逆,當(dāng)要監(jiān)聽的 View 數(shù)量較多時进胯,這種方式好像就不太友好了,此時我們便可以使用如下方式原押,監(jiān)聽整個視圖樹內(nèi) View 焦點的變化:

mSplash.getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
    @Override
    public void onGlobalFocusChanged(View oldFocus, View newFocus) {
       // 監(jiān)聽整個視圖樹內(nèi)的View焦點變化
    }
});
4. OnGlobalLayoutListener

OnGlobalLayoutListener 表示視圖樹中的的布局狀態(tài)或視圖的可見性更改時回調(diào)胁镐。在 ViewTreeObserver 內(nèi)的分發(fā)過程如下:

/**
 * 通知已注冊的偵聽器發(fā)生了全局布局。如果您正在強制視圖或視圖層次結(jié)構(gòu)上的布局不附加到窗口或處于消失狀態(tài)诸衔,則可以手動調(diào)用此方法
 */
public final void dispatchOnGlobalLayout() {
    final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
    if (listeners != null && listeners.size() > 0) {
        CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
        try {
            int count = access.size();
            for (int i = 0; i < count; i++) {
                // 回調(diào) onGlobalLayout 方法
                access.get(i).onGlobalLayout();
            }
        } finally {
            listeners.end();
        }
    }
}

注意保存 OnGlobalLayoutListener 的容器是 CopyOnWriteArray盯漂,而非 CopyOnWriteArrayList,前者是 ViewTreeObserver 的靜態(tài)內(nèi)部類笨农,后者是 Java 內(nèi)置的并發(fā)工具類就缆,兩者有什么區(qū)別嗎?CopyOnWriteArray 在沒有遍歷任務(wù)時(未調(diào)用 start())磁餐,此時執(zhí)行 add 操作直接添加到原容器,否則才會執(zhí)行 CopyOnWrite,這有助于減少內(nèi)存抖動場景的發(fā)生诊霹。

如果僅從名字來看羞延,它應(yīng)該和 View 的 layout 階段有關(guān),下面就再看下 View 的繪制流程:

private void  performTraversals{
    // ... 省略

    // View的測量階段
    performMeasure();
    // View 的布局階段
    performLayout();

    ...
    // 關(guān)鍵方法在這里
    if (triggerGlobalLayoutListener) {
        mAttachInfo.mRecomputeGlobalAttributes = false;
        // 布局階段完成之后回調(diào) OnGlobalLayoutListener脾还,
        // 在該方法中可以正確獲取 View 的實際寬高
        mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
     }  

     ...
    
     // View 的繪制階段
     performDraw();

 }

dispatchOnGlobalLayout() 的回調(diào)是在 performLayout 方法之后伴箩,此時視圖樹的布局階段已經(jīng)完成,在該方法我們可以正確獲取到 View 的實際寬高鄙漏,具體你可以參考《深入 Activity 三部曲(3)之 View 繪制流程》嗤谚。

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            // 正確獲取到View的寬高
            int width = view.getWidth();
            int height = view.getHeight();
        }
});

注意此時 View 的繪制任務(wù)還未開始!

5. OnPreDrawListener

OnPreDrawListener 表示當(dāng)視圖即將繪制時回調(diào)怔蚌,它在 ViewTreeObserver 的分發(fā)機制如下:

public final boolean dispatchOnPreDraw() {
        boolean cancelDraw = false;
        final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
        if (listeners != null && listeners.size() > 0) {
            // 調(diào)用了 start巩步,此時如果有其他線程要 add 內(nèi)容
            // 就會執(zhí)行copyOnWrite
            CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    // 回調(diào) onPreDraw 方法
                    cancelDraw |= !(access.get(i).onPreDraw());
                }
            } finally {
                listeners.end();
            }
        }
       return cancelDraw;
 }

OnPreDraw 和 OnDrawListener 兩者有什么區(qū)別呢?Pre 表達的應(yīng)該是 Previous桦踊,即在 Draw 之前椅野,直接看下 View 的繪制任務(wù):

// 調(diào)用 AttachInfo 內(nèi) ViewTreeObserver 的 dispatchOnPreDraw方法
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
// 只有有一個 Listener 返回true,將重新發(fā)起繪制流程
 if (!cancelDraw && !newSurface) {
      if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
           for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
       }

        //執(zhí)行繪制操作
        performDraw();
  }else {
      if (isViewVisible) {
           // 重新發(fā)起繪制流程籍胯,measure竟闪、layout 
           scheduleTraversals();
       } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).endChangingAnimations();
            }
           mPendingTransitions.clear();
       }
   }

如果 dispatchOnPreDraw() 方法返回 true,此時將攔截 performDraw() 的繪制任務(wù)杖狼,重新發(fā)起繪制流程 scheduleTraversals()炼蛤。

其實它的主要作用是在繪制任務(wù)之前給一次攔截機會,例如有新的 View 需要添加到當(dāng)前視圖樹中蝶涩,此時可以攔截該次繪制任務(wù)理朋,重新發(fā)起 measure、layout 流程子寓。

6. OnDrawListener

OnDrawListener 與 OnPreDrawListener 類似暗挑,后者可以攔截繪制任務(wù),重新發(fā)起繪制流程斜友,在 ViewTreeObserver 的分發(fā)機制如下:

/**
   * 通知已注冊的偵聽器繪圖即將開始炸裆。
   */
  public final void dispatchOnDraw() {
      if (mOnDrawListeners != null) {
          mInDispatchOnDraw = true;
          final ArrayList<OnDrawListener> listeners = mOnDrawListeners;
          int numListeners = listeners.size();
          for (int i = 0; i < numListeners; ++i) {
              // 通知View開始繪制
              listeners.get(i).onDraw();
          }
          mInDispatchOnDraw = false;
      }
  }

OnDraw,它肯定和 View 的繪制任務(wù)有關(guān)鲜屏!還是要從 ViewRootImpl 入手烹看,View 的繪制任務(wù) performDraw 方法最終會調(diào)用到 draw():

private void draw(boolean fullRedrawNeeded) {
    // 窗口都關(guān)聯(lián)有一個 Surface
    // 在 Android 中,所有的元素都在 Surface 這張畫紙上進行繪制和渲染洛史,
    // 普通 View(例如非 SurfaceView 或 TextureView) 是沒有 Surface 的惯殊,
    // 一般 Activity 包含多個 View 形成 View Hierachy 的樹形結(jié)構(gòu),只有最頂層的 DecorView 才是對 WindowManagerService “可見的”也殖。
    // 而為普通 View 提供 Surface 的正是 ViewRootImpl土思。
    Surface surface = mSurface;
    if (!surface.isValid()) {
        // Surface 是否還有效
        return;
    }

    // 跟蹤FPS
    if (DEBUG_FPS) {
        trackFPS();
    }

    ...

    scrollToRectOrFocus(null, false);

    if (mAttachInfo.mViewScrollChanged) {
        mAttachInfo.mViewScrollChanged = false;
        // 注意 OnScrollChangedListener 在這里回調(diào)
        mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
    }

    ...

    // 通知View開始繪制
    mAttachInfo.mTreeObserver.dispatchOnDraw();

    ...

    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            // 是否開啟了硬件加速务热,
            boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested;
            ...

            // 開啟硬件加速繪制執(zhí)行這里,最終還是執(zhí)行View的draw開始
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
        } else {               
            ...

            // 最終調(diào)用到drawSoftware
            // surface己儒,每個 View 都由某一個窗口管理崎岂,而每一個窗口都關(guān)聯(lián)有一個 Surface
            // mDirty.set(0, 0, mWidth, mHeight); dirty 表示畫紙尺寸,對于DecorView闪湾,left = 0,
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
    }

    ...
}

注意如下代碼調(diào)用:

// 回調(diào) ViewTreeObserver內(nèi)的OnDrawListener
mAttachInfo.mTreeObserver.dispatchOnDraw() 冲甘;

需要注意,此時 View 繪制流程的前兩個階段:measure 和 layout 都已經(jīng)完成途样。通過該 Listener 可以監(jiān)聽開始執(zhí)行繪制任務(wù)江醇。

7. OnTouchModeChangeListener

OnTouchModeChangeListener 表示當(dāng)觸摸模式改變時回調(diào),它在 ViewTreeObserver 的分發(fā)機制如下:

/**
 * 通知已注冊的偵聽器觸摸模式已更改何暇。
 */
final void dispatchOnTouchModeChanged(boolean inTouchMode) {
    final CopyOnWriteArrayList<OnTouchModeChangeListener> listeners =
                mOnTouchModeChangeListeners;
    if (listeners != null && listeners.size() > 0) {
        for (OnTouchModeChangeListener listener : listeners) {
            // 回調(diào) onTouchModeChanged 方法
            listener.onTouchModeChanged(inTouchMode);
        }
    }
}

前面分析 OnWindowFocusChangeListener 的回調(diào)是通過 ViewRootImpl 靜態(tài)內(nèi)部類 W 完成的陶夜,在該類中 windowFocusChanged 方法接收遠程 WMS 的回調(diào)通知,發(fā)送消息到當(dāng)前繪制線程:

case MSG_WINDOW_FOCUS_CHANGED: {

    if(hasWindowFocus){
        boolean inTouchMode = msg.arg2 != 0
        // 這里將回調(diào) OnTouchModeChangeListener
       ensureTouchModeLocally(inTouchMode)
    }
    ... 下面回調(diào) dispatchWindowFocusChanged 方法
}

ensureTouchModeLocally 方法將會完成該過程:

private boolean ensureTouchModeLocally(boolean inTouchMode) {
    if (mAttachInfo.mInTouchMode == inTouchMode) return false;

    mAttachInfo.mInTouchMode = inTouchMode;
    // 回調(diào)AttachInfo內(nèi)ViewTreeObserver的dispatchOnTouchModeChanged方法
    mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);

    return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
}
8. OnScrollChangedListener 回調(diào)時機

OnScrollChangeListener 表示當(dāng)視圖樹中的某些內(nèi)容被滾動時回調(diào)赖晶,它在 ViewTreeObserver 的分發(fā)機制如下:

/**
 * 通知已注冊的偵聽器某些內(nèi)容已滾動律适。
 */
final void dispatchOnScrollChanged() {
    final CopyOnWriteArray<OnScrollChangedListener> listeners = mOnScrollChangedListeners;
    if (listeners != null && listeners.size() > 0) {
        CopyOnWriteArray.Access<OnScrollChangedListener> access = listeners.start();
        try {
            int count = access.size();
            for (int i = 0; i < count; i++) {
                // 回調(diào) onScrollChanged
                access.get(i).onScrollChanged();
            }
        } finally {
            listeners.end();
        }
    }
}

是否有注意到前面分析 OnDrawListener 時的繪制方法 draw,在調(diào)用 ViewTreeObserver 的 dispatchOnDraw 方法之前遏插,會根據(jù)情況率先執(zhí)行 ViewTreeObserver 的 dispachOnScrollChanged()捂贿。

  • OnScrollChangedListener 回調(diào)在 OnDrawListener 之前,用于通知外部當(dāng)前視圖樹中有 View 發(fā)生滾動胳嘲。

以上便是個人在學(xué)習(xí) ViewTreeObserver 時的體會和總結(jié) 厂僧,文中如有不妥或有更好的分析結(jié)果,歡迎您的指正了牛。

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


擴展閱讀

其他系列專題

Android 存儲優(yōu)化系列專題
Android 之不要濫用 SharedPreferences
Android 之不要濫用 SharedPreferences(2)— 數(shù)據(jù)丟失
Android 對象序列化之你不知道的 Serializable
Android 對象序列化之 Parcelable 取代 Serializable ?
Android 對象序列化之追求完美的 Serial

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載甫窟,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末蛙婴,一起剝皮案震驚了整個濱河市粗井,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌街图,老刑警劉巖浇衬,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異餐济,居然都是意外死亡耘擂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門絮姆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來醉冤,“玉大人秩霍,你說我怎么就攤上這事∫涎簦” “怎么了前域?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我魁蒜,道長倾哺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任溢陪,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糠悼。我一直安慰自己,他們只是感情好浅乔,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布倔喂。 她就那樣靜靜地躺著,像睡著了一般靖苇。 火紅的嫁衣襯著肌膚如雪席噩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天贤壁,我揣著相機與錄音悼枢,去河邊找鬼。 笑死脾拆,一個胖子當(dāng)著我的面吹牛馒索,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播名船,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼绰上,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了渠驼?” 一聲冷哼從身側(cè)響起蜈块,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渴邦,沒想到半個月后疯趟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡谋梭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年信峻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓮床。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡盹舞,死狀恐怖产镐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情踢步,我是刑警寧澤癣亚,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站获印,受9級特大地震影響述雾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜兼丰,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一玻孟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鳍征,春花似錦黍翎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氮双,卻和暖如春碰酝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背戴差。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工砰粹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人造挽。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓碱璃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親饭入。 傳聞我的和親對象是個殘疾皇子嵌器,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345