安卓 ViewTreeObserver源碼分析

今天在寫程序時報了一個這樣的錯:

This ViewTreeObserver is not alive, call getViewTreeObserver() again

原因是我在Activity的onCreate方法中這樣寫的:

ViewTreeObserver vto = recyclerview.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //do some thing
                vto.removeOnGlobalLayoutListener(this);
            }
        });

先給出解決方法,只需要把

vto.removeOnGlobalLayoutListener(this);

替換成

recyclerview.getViewTreeObserver().removeOnGlobalLayoutListener(this);

引出問題之後,我們開始研究下源代碼,從以下幾個方面去看:
1、ViewTreeObserver是什麼?是如何組織起來的?有什麼用?
2、爲什麼在ViewTreeObserver.addOnGlobalLayoutListener中可以獲取到控件的寬高。

一、ViewTreeObserver是什麼?

按照源碼中的解釋,ViewTreeObserver就是用來註冊一些ViewTree的一些全局事件監聽,全局事件包括layout開始、draw開始、觸摸模式等等。
也就是說ViewTreeObserver就是主要用來監聽這些事件的,關於這個事件的監聽、註冊、反註冊有一大推方法
在這裏插入圖片描述
瞭解完之後,就要將這個東西和View掛鉤了,但其實這個東西和View的關係其實沒有那麼大,在View這個類的內部有一個AttachInfo的類,這個類主要是用來記錄當整個view樹attach到window時的信息,挑幾個成員看看:

final IWindow mWindow;
/**
* The top view of the hierarchy.
 */
 View mRootView;
 /**
* Left position of this view's window
 */
int mWindowLeft;
/**
* Top position of this view's window
*/
int mWindowTop;
final ViewTreeObserver mTreeObserver;
/**
* The view root impl.
*/
final ViewRootImpl mViewRootImpl;

可以看出這些成員都是和window、ViewRootImpl相關的,其中就有mTreeObserver,然後在View內部有個AttachInfo類的引用mAttachInfo,因此View和ViewTreeObserver產生了聯繫。
現在就來查找mAttachInfo是在哪裏賦值的了,發現賦值的地方只有一個,天助我也:

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
        }
       ......
   }

dispatchAttachedToWindow這個方法其實在ViewGroup中重載了,根據ViewGroup中代碼的形式就能猜到這個過程和measure、layout、draw一模一樣,就是遞歸調用view樹,直到葉子結點。AttachInfo info這是作爲一個參數傳遞過來的,那麼就要找到第一個調用這個方法的位置,由於這東西是和window的attach相關的,那麼可以肯定當把頂層view加入到window中,然後調用這個方法,這裏就直接給出調用位置了,在ViewImpl.performTraversals這個方法中,performTraversals這個方法巨長,有七百多行,裏面包含了非常重要的幾個階段,上代碼:

private void performTraversals(){
	.......
	if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;

            final Configuration config = mContext.getResources().getConfiguration();
            if (shouldUseDisplaySize(lp)) {
	            ......
	            //host就是頂層view,即decorview
                host.dispatchAttachedToWindow(mAttachInfo, 0);
                ......
             }
             ......
             //啓動測量
             performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
             ......
             //啓動layout
             performLayout(lp, mWidth, mHeight);
             ......
             if (triggerGlobalLayoutListener) {
            	mAttachInfo.mRecomputeGlobalAttributes = false;
            	mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
            }
            .......
            boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
            ........
            performDraw();
        }
}

從上面的流程已經非常清楚整個過程了,首先dcrorview調用dispatchAttachedToWindow,將AttachInfo這個參數一直傳遞下去,使得整個view樹中其實只有一個AttachInfo對象,然後measure、layout,接着就調用我們設置的監聽了,因爲是在layout之後,所有我們可以得到正確的寬高.

二、問題分析

有了上面的分析,下面關於開頭的那個問題就很好解決了,首先我們看獲取ViewTreeObserver的代碼:

public ViewTreeObserver getViewTreeObserver() {
        if (mAttachInfo != null) {
            return mAttachInfo.mTreeObserver;
        }
        if (mFloatingTreeObserver == null) {
            mFloatingTreeObserver = new ViewTreeObserver(mContext);
        }
        return mFloatingTreeObserver;
    }

上面出現了mFloatingTreeObserver ,因爲我們這個獲取的代碼是寫在Activity.onCreate裏面的,這時mAttachInfo其實還是空,沒有被賦值,說到這先叉開一段,將performTraversals和Activity的創建流程的流程說明一下,順序如下:

Activity.onCerate() -->
Activity.onStart() --> 
Activity.onResume() --> 
在這裏會把DecorView加入window,然後window其實也不管理view,交給
ViewImpl,調用ViewImpl.setView -->
ViewImpl.performTraversals()

知道上面的流程後,我們就知道在onCreate裏面獲取到的其實是mFloatingTreeObserver (在這裏命名很有意思,float就是浮動,說明後來可能要修改),然後我們在這個ViewTreeObserver上註冊了一個listener,當DecorView調用dispatchAttachedToWindow時,就會將mFloatingTreeObserver 置爲空的,具體是在View.dispatchAttachedToWindow:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
        ........
        if (mFloatingTreeObserver != null) {
            info.mTreeObserver.merge(mFloatingTreeObserver);
            mFloatingTreeObserver = null;
}

很明顯了,因爲整個view樹都只是引用同一個AttachInfo,如果mFloatingTreeObserver 不爲空,那麼就把mFloatingTreeObserver 置爲空,並且把mFloatingTreeObserver 上面註冊的listener加到AttachInfo中,具體加入邏輯在info.mTreeObserver.merge中,merge方法中把listener重新加入到新的AttachInfo中,最後,還調用了

observer.kill();

其實就是將ViewTreeObserver.mAlive置爲false。
到此爲止,流程已全部分析清楚,現在可以回過頭來解決開頭的問題:

ViewTreeObserver vto = recyclerview.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //do some thing
                vto.removeOnGlobalLayoutListener(this);
            }
        });

上面的vto在調用onGlobalLayout之前就已經無效了,所有view的attachInfo都指向了一個對象,至於異常在哪拋出的,答案盡在removeOnGlobalLayoutListener這個方法中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章