Android中慎用View#getViewTreeObserver#addOnGlobalLayoutListener來獲取view的高度

背景

咱們常常在view初始化的時候想要獲取view的大小,好比在Activity的onCreate方法中想要取得view的大小,有不少小夥伴知道能夠在View#getViewTreeObserver#addOnGlobalLayoutListener回調中獲得結果,取到結果後,咱們通常都會remove掉這個listener,即調用View#getViewTreeObserver#removeOnGlobalLayoutListener或View#getViewTreeObserver#removeGlobalOnLayoutListener兩個方法。java

先說下緣由

View#getViewTreeObserver每次獲得的ViewTreeObserver可能不是同一個實例android

看幾個例子

  • 場景一
package com.lkh.viewtreeobservertest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;

public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final View text = findViewById(R.id.text);
        ViewTreeObserver viewTreeObserver1 = text.getViewTreeObserver();

        Log.i(TAG, "viewTreeObserver1="+viewTreeObserver1 + " text h="+text.getHeight());
        viewTreeObserver1.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                ViewTreeObserver viewTreeObserver2 = text.getViewTreeObserver();
// viewTreeObserver.removeOnGlobalLayoutListener(this);
                Log.i(TAG, "viewTreeObserver2="+ viewTreeObserver2 + " onGlobalLayout text h="+text.getHeight());
            }
        });

    }
}

複製代碼

第一次進入Activity時打印的log:bash

05-13 13:46:46.737 29528-29528/com.lkh.viewtreeobservertest I/MainActivity: viewTreeObserver1=android.view.ViewTreeObserver@9b3ac41 text h=0
05-13 13:46:46.807 29528-29528/com.lkh.viewtreeobservertest I/MainActivity: viewTreeObserver2=android.view.ViewTreeObserver@370a279 onGlobalLayout text h=57
05-13 13:46:46.827 29528-29528/com.lkh.viewtreeobservertest I/MainActivity: viewTreeObserver2=android.view.ViewTreeObserver@370a279 onGlobalLayout text h=57
複製代碼

咱們看到兩次獲取到的ViewTreeObserver不同。第一次拿到的是ViewTreeObserver@9b3ac41,在onGlobalLayout回調方法中拿到的是ViewTreeObserver@370a279app

  • 場景二
package com.lkh.viewtreeobservertest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;

public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final View text = findViewById(R.id.text);
        ViewTreeObserver viewTreeObserver1 = text.getViewTreeObserver();

        Log.i(TAG, "viewTreeObserver1="+viewTreeObserver1 + " text h="+text.getHeight());
        viewTreeObserver1.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                ViewTreeObserver viewTreeObserver2 = text.getViewTreeObserver();
                viewTreeObserver2.removeOnGlobalLayoutListener(this);
                Log.i(TAG, "viewTreeObserver2="+ viewTreeObserver2 + " onGlobalLayout text h="+text.getHeight());
            }
        });

    }
}
複製代碼

第一次進入Activity時打印的log:ide

05-13 13:50:01.227 29733-29733/com.lkh.viewtreeobservertest I/MainActivity: viewTreeObserver1=android.view.ViewTreeObserver@9b3ac41 text h=0
05-13 13:50:01.277 29733-29733/com.lkh.viewtreeobservertest I/MainActivity: viewTreeObserver2=android.view.ViewTreeObserver@370a279 onGlobalLayout text h=57
複製代碼

跟場景一基本同樣,只是在onGlobalLayout中添加了viewTreeObserver2.removeOnGlobalLayoutListener(this),能夠看到移除成功了,onGlobalLayout只執行了一次。oop

  • 場景三
package com.lkh.viewtreeobservertest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;

public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final View text = findViewById(R.id.text);
        final ViewTreeObserver viewTreeObserver1 = text.getViewTreeObserver();

        Log.i(TAG, "viewTreeObserver1="+viewTreeObserver1 + " text h="+text.getHeight());
        viewTreeObserver1.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                viewTreeObserver1.removeOnGlobalLayoutListener(this);

                ViewTreeObserver viewTreeObserver2 = text.getViewTreeObserver();
                Log.i(TAG, "viewTreeObserver2="+ viewTreeObserver2 + " onGlobalLayout text h="+text.getHeight());
            }
        });

    }
}
複製代碼

第一次進入Activity時打印的log:佈局

05-13 13:54:02.157 30070-30070/com.lkh.viewtreeobservertest I/MainActivity: viewTreeObserver1=android.view.ViewTreeObserver@9b3ac41 text h=0
05-13 13:54:02.217 30070-30070/com.lkh.viewtreeobservertest E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.lkh.viewtreeobservertest, PID: 30070
    java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
        at android.view.ViewTreeObserver.checkIsAlive(ViewTreeObserver.java:887)
        at android.view.ViewTreeObserver.removeOnGlobalLayoutListener(ViewTreeObserver.java:599)
        at com.lkh.viewtreeobservertest.MainActivity$1.onGlobalLayout(MainActivity.java:23)
        at android.view.ViewTreeObserver.dispatchOnGlobalLayout(ViewTreeObserver.java:982)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2462)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1489)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7472)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
        at android.view.Choreographer.doCallbacks(Choreographer.java:686)
        at android.view.Choreographer.doFrame(Choreographer.java:622)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:7224)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
複製代碼

直接removeOnGlobalLayoutListener的時候拋異常了this

分析緣由

查找緣由最好的辦法就是直接去看源代碼了,咱們直接先看View#getViewTreeObserver的源碼(android源碼看的是sdk28的,每一個版本可能有不同)spa

View#getViewTreeObservercode

public ViewTreeObserver getViewTreeObserver() {
    if (mAttachInfo != null) {
        return mAttachInfo.mTreeObserver;
    }
    if (mFloatingTreeObserver == null) {
        mFloatingTreeObserver = new ViewTreeObserver(mContext);
    }
    return mFloatingTreeObserver;
}
複製代碼
  1. 在上面的三個場景上,Activity#onCreate中,mAttachInfo爲null(爲何爲null咱們之後分析),因此咱們在Activity#onCreate中第一次View#getViewTreeObserver拿到的是mFloatingTreeObserver
  2. 在回調方法onGlobalLayout中,mAttachInfo不爲null(爲何不爲null先不展開說),View#getViewTreeObserver拿到的就是mAttachInfo.mTreeObserver

由上面兩條咱們知道了爲何場景一中兩次拿到的ViewTreeObserver不一樣的緣由。

onGlobalLayout調用的時機

下面咱們來看下onGlobalLayout調用的時機。

先在ViewTreeObserver找到調用onGlobalLayout地方,ViewTreeObserver中跟本次分析有關的源碼以下

ViewTreeObserver#dispatchOnGlobalLayout

public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
        checkIsAlive();

        if (mOnGlobalLayoutListeners == null) {
            mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
        }

        mOnGlobalLayoutListeners.add(listener);
    }

    public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
        checkIsAlive();
        if (mOnGlobalLayoutListeners == null) {
            return;
        }
        mOnGlobalLayoutListeners.remove(victim);
    }
    
    private void checkIsAlive() {
        if (!mAlive) {
            throw new IllegalStateException("This ViewTreeObserver is not alive, call "
                    + "getViewTreeObserver() again");
        }
    }
    
    public final void dispatchOnGlobalLayout() {
        // 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.
        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++) {
                    //調用方法在這裏
                    access.get(i).onGlobalLayout();
                }
            } finally {
                listeners.end();
            }
        }
    }
複製代碼

咱們看到調用onGlobalLayout方法的是dispatchOnGlobalLayout方法,再繼續找到調用dispatchOnGlobalLayout方法的地方,是在ViewRootImpl#performTraversals方法中調用,以下:

android.view.ViewRootImpl#performTraversals

private void performTraversals() {
    
        if (mFirst) {
            ...
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            ...
        
        }
        ...
        
        if (...){
            ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
        }
        
        ...
    
        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
        }
        
        ...
    
        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
        
        ...
    }
複製代碼

咱們看到這裏調用的是mAttachInfo.mTreeObserver裏的dispatchOnGlobalLayout,而咱們在Activity#onCreate方法中第一次getViewTreeObserver拿到的是mFloatingTreeObserver,並非mAttachInfo.mTreeObserver,那咱們往mFloatingTreeObserver中addOnGlobalLayoutListener怎麼會執行回調呢,咱們在View源碼中查看下使用mFloatingTreeObserver的地方,在View#dispatchAttachedToWindow中找到了它的身影,以下:

View#dispatchAttachedToWindow

/** * @param info the {@link android.view.View.AttachInfo} to associated with * this view */
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;

        ...

        if (mFloatingTreeObserver != null) {
            info.mTreeObserver.merge(mFloatingTreeObserver);
            mFloatingTreeObserver = null;
        }

        ...
    }
複製代碼

原來是在這裏把它合併到AttachInfo的mTreeObserver中了,看下ViewTreeObserver#merge方法,以下:

ViewTreeObserver#merge

void merge(ViewTreeObserver observer) {
        
        ...

        if (observer.mOnGlobalLayoutListeners != null) {
            if (mOnGlobalLayoutListeners != null) {
                mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
            } else {
                mOnGlobalLayoutListeners = observer.mOnGlobalLayoutListeners;
            }
        }

        ...

        observer.kill();
    }
複製代碼

看到了吧,直接addAll了,所有添加到了AttachInfo的mTreeObserver中。那如今還有個問題,怎麼保證合併到AttachInfo的mTreeObserver中的執行時機在mAttachInfo.mTreeObserver.dispatchOnGlobalLayout()執行前,其它咱們在上面的源碼中已經看到了它們的執行順序,這裏咱們再看一下,以下:

android.view.ViewRootImpl#performTraversals

private void performTraversals() {
    
        if (mFirst) {
            ...
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            ...
        
        }
        ...
        
        if (...){
            ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
        }
        
        ...
    
        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
        }
        
        ...
    
        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
        
        ...
    }
複製代碼

咱們看到,順序依次是dispatchAttachedToWindow、performMeasure、performLayout、dispatchOnGlobalLayout。發現dispatchOnGlobalLayout是在這幾個方法的最後面執行,由此能夠知道:

  1. 執行dispatchOnGlobalLayout時,已經合併到了AttachInfo的mTreeObserver中
  2. 執行在onGlobalLayout中能夠拿到view的寬高,由於performMeasure(測量)、performLayout(佈局)已經先執行

再來看下場景三中爲何會拋出異常

先看下log

05-13 13:54:02.217 30070-30070/com.lkh.viewtreeobservertest E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.lkh.viewtreeobservertest, PID: 30070
    java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
        at android.view.ViewTreeObserver.checkIsAlive(ViewTreeObserver.java:887)
        at android.view.ViewTreeObserver.removeOnGlobalLayoutListener(ViewTreeObserver.java:599)
        at com.lkh.viewtreeobservertest.MainActivity$1.onGlobalLayout(MainActivity.java:23)
        at android.view.ViewTreeObserver.dispatchOnGlobalLayout(ViewTreeObserver.java:982)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2462)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1489)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7472)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
        at android.view.Choreographer.doCallbacks(Choreographer.java:686)
        at android.view.Choreographer.doFrame(Choreographer.java:622)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:7224)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
複製代碼

回顧代碼以下:

final ViewTreeObserver viewTreeObserver1 = text.getViewTreeObserver();

viewTreeObserver1.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        viewTreeObserver1.removeOnGlobalLayoutListener(this);

        ViewTreeObserver viewTreeObserver2 = text.getViewTreeObserver();
        Log.i(TAG, "viewTreeObserver2="+ viewTreeObserver2 + " onGlobalLayout text h="+text.getHeight());
    }
});
複製代碼

咱們在onGlobalLayout中使用的ViewTreeObserver爲第一次獲取到的mFloatingTreeObserver,調用removeOnGlobalLayoutListener方法時,會先執行checkIsAlive方法,這裏拋異常,mAlive確定爲false。

ViewTreeObserver#removeOnGlobalLayoutListener

public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
        checkIsAlive();
        if (mOnGlobalLayoutListeners == null) {
            return;
        }
        mOnGlobalLayoutListeners.remove(victim);
    }
    
    private void checkIsAlive() {
        if (!mAlive) {
            throw new IllegalStateException("This ViewTreeObserver is not alive, call "
                    + "getViewTreeObserver() again");
        }
    }
複製代碼

哪裏會把mAlive置爲false呢,找到以下代碼:

ViewTreeObserver#kill

/** * Marks this ViewTreeObserver as not alive. After invoking this method, invoking * any other method but {@link #isAlive()} and {@link #kill()} will throw an Exception. * * @hide */
    private void kill() {
        mAlive = false;
    }
複製代碼

而執行kill方法的地方在哪呢,能夠看到前面執行merge方法時,就執行了kill方法,再回顧下:

ViewTreeObserver#merge

void merge(ViewTreeObserver observer) {
        
        ...

        if (observer.mOnGlobalLayoutListeners != null) {
            if (mOnGlobalLayoutListeners != null) {
                mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
            } else {
                mOnGlobalLayoutListeners = observer.mOnGlobalLayoutListeners;
            }
        }

        ...

        observer.kill();
    }
複製代碼

看到沒,最後執行了kill方法,因此場景三會直接拋異常。 場景二中能remove成功的緣由經過上面的分析應該已經很清楚了,就很少說了。

總結

經過上面的代碼分析,咱們知道:

  1. 每次調用View#getViewTreeObserver獲得的實例可能不同,因此remove的時候不必定能移除。
  2. 直接使用第一次拿到的View#getViewTreeObserver而後保存起來,remove的時候直接用同一個,有可能拋異常。

綜上,使用View#getViewTreeObserver的回調方法必定要當心,頗有可能出現意想不到的現象,慎用。

掃一掃 關注個人公衆號 【老羅開發】
相關文章
相關標籤/搜索