今天在寫程序時報了一個這樣的錯:
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這個方法中。