ViewTreeObserver源码学习

Posted by JackPeng on June 23, 2017

一、理解ViewTreeObserver概念

ViewTreeObserver用来注册监听器,在视图树全局发生变化时收到通知。它不能被应用实例化,因为它是由视图提供,通过android.view.View#getViewTreeObserver()来获取。

ViewTree:视图树。在Android中,所有视图由View和View的子类组成。ViewGroup也是view的子类,它是View的容器,它可以装载View和ViewGroup。这样ViewGroup和View以树形结构一层一层的嵌套组合,就形成了视图树。

Observer:观察者。使用了观察者的设计模式,在这里,即ViewTree时被观察者,ViewTreeObserver是观察者,通过ViewTreeObserver注册监听来观察ViewTree的变化,当ViewTree发生变化,就会调用ViewTreeObserver的相关方法来通知其这一改变。我们可以在ViewTreeObserver中add自己的监听器,从而得到ViewTree的某一变化的通知做出自己的逻辑处理。

二、如何获取ViewTreeObserver

ViewTreeObserver是不能被应用程序实例化的,因为它是由视图提供的,通过view.getViewTreeObserver()获取。

//View.java

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

其中,AttachInfo是View的一个内部类。当一个View附着到它的父Window中时,这个View能获取到一组View和父Window之间的信息,就存储在AttachInfo当中。AttachInfo中就有一个ViewTreeObserver对象。当mAttachInfo为空时,返回mFloatingTreeObserver,一个特殊的ViewTreeObserver。

那么,每一个view的mAttachInfo是怎么获取到的呢?AttachInfo是在View第一次attach到Window时,ViewRoot传给自己的子View的,然后沿着视图树,AttachInfo会一直传递到每一个View。

//ViewGroup.java

@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
      ...
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    ...
}

//View.java

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    //System.out.println("Attached! " + this);
    mAttachInfo = info;
    ...
}

另外,当新的View加入到ViewGroup中时,也会将AttachInfo传入。

//ViewGroup.java

 private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    ...
    AttachInfo ai = mAttachInfo;
    if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
        boolean lastKeepOn = ai.mKeepScreenOn;
        ai.mKeepScreenOn = false;
        child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
        if (ai.mKeepScreenOn) {
            needGlobalAttributesUpdate(true);
        }
        ai.mKeepScreenOn = lastKeepOn;
    }
    ...

}

三、ViewTreeObserver的可用性

通过View.getViewTreeObserver()获得的ViewTreeObserver并不能保证在View的整个生命周期中一直是存活的。所以如果我们调用这个方法的时候,要长期引用这个ViewTreeObserver的话,在使用ViewTreeObserver之前就需要通过isAlive()方法去检查它是否是可用的。

//ViewTreeObserver.java

public boolean isAlive() {
    return mAlive;
}

其实我们在调用ViewTreeObserver的addOnXxxListener和removeOnXxxListener时,这些方法内部执行的一个操作就是checkIsAlive()去判断ViewTreeObserver是否可用。当ViewTreeObserver不可用时,将抛出IllegalStateException异常,并提示重新通过getViewTreeObserver()获取ViewTreeObserver。

//ViewTreeObserver.java

private void checkIsAlive() {
    if (!mAlive) {
        throw new IllegalStateException("This ViewTreeObserver is not alive, call "
                + "getViewTreeObserver() again");
    }
}

四、走入ViewTreeObserver类内部

前面提到,ViewTreeObserver用来注册监听器,在视图树全局发生变化时收到通知。目前ViewTreeObserver可以监听11种全局事件,对应了11个内部类,以下8个是公开的。

 /**
 * 当视图树attached/detached到window时,回调的接口类定义
 */
public interface OnWindowAttachListener 


/**
 * 当window的焦点状态发生变化时,回调的接口类定义
 */
public interface OnWindowFocusChangeListener 


/**
 * 当视图树的焦点状态发生变化时,回调的接口类定义
 */
public interface OnGlobalFocusChangeListener 


/**
 * 当视图树的全局布局状态发生变化或者视图树中某个view的可见状态发生变化时,回调的接口类定义
 */
public interface OnGlobalLayoutListener 


/**
 * 当视图树将要绘制时,回调的接口类定义
 */
public interface OnPreDrawListener 


/**
 * 当视图树绘制时,回调的接口类定义
 */
public interface OnDrawListener


/**
 * 当触摸模式发生变化时,回调的接口类定义
 */
public interface OnTouchModeChangeListener


/**
 * 当视图树中某些组件发生滚动时,回调的接口类定义
 */
public interface OnScrollChangedListener

对于以上每一种接口,又对应:

  • 1 对象集合:mOnXxxListeners
  • 2 增加监听: addOnXxxListener(OnXxxListener listener)
  • 3 移出监听: removeOnXxxListener(OnXxxListener victim)
  • 4 分发事件: dispatchOnXxx()

五、ViewTreeObserver使用示例

接下来我们具体来看一个使用流程。比如,我们要在onCreate中测量一个控件的宽高。

以下是一种通过OnPreDrawListener来计算图片宽高的实现。通过imageView.getViewTreeObserver()获取ViewTreeObserver,然后增加一个自定义的ViewTreeObserver.OnPreDrawListener,覆写onPreDraw()方法。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ImageView imageView = (ImageView) findViewById(R.id.imageview);
    imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                public boolean onPreDraw() {
                    int height = imageView.getMeasuredHeight();
                    int width = imageView.getMeasuredWidth();
                    imageView.getViewTreeObserver().removeOnPreDrawListener(this);
                    return true;
                }
            });
}

首先看看addOnPreDrawListener方法。第一步,检测ViewTreeObserver是否可用,不可用的话抛出异常提示再获取一次。可用的话,则将这个listener添加到mOnPreDrawListeners(所有OnPreDrawListener对象的集合)当中。

//ViewTreeObserver.java

public void addOnPreDrawListener(OnPreDrawListener listener) {
    checkIsAlive();

    if (mOnPreDrawListeners == null) {
        mOnPreDrawListeners = new CopyOnWriteArray<OnPreDrawListener>();
    }

    mOnPreDrawListeners.add(listener);
}

为了防止多次收到通知调用onPreDraw,就需要在合适的时候移除这个监听。同样,也是先检测ViewTreeObserver是否可用,不可用的话抛出异常提示再获取一次。可用的话,则将这个listener从mOnPreDrawListeners删除掉。

//ViewTreeObserver.java

public void removeOnPreDrawListener(OnPreDrawListener victim) {
    checkIsAlive();
    if (mOnPreDrawListeners == null) {
        return;
    }
    mOnPreDrawListeners.remove(victim);
}

使用方法就此完成。在内部,ViewTreeObserver还需要负责分发事件通知给相应的监听对象,由dispatchOnXxx方法来执行。dispatchOnXxx方法一般由系统来调用。可以看到在dispatchOnPreDraw方法中,会去遍历mOnPreDrawListeners,然后逐个调用OnPreDrawListener.onPreDraw方法。

//ViewTreeObserver.java

public final boolean dispatchOnPreDraw() {
    boolean cancelDraw = false;
    final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
    if (listeners != null && listeners.size() > 0) {
        CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
        try {
            int count = access.size();
            for (int i = 0; i < count; i++) {
                cancelDraw |= !(access.get(i).onPreDraw());
            }
        } finally {
            listeners.end();
        }
    }
    return cancelDraw;
}

其他几个OnXXXListener使用及原理基本同上。

六、View的绘制过程中dispatchOnXxx方法的调用时机

在实际应用中,虽然知道几个监听事件的大致概念,但怎么使用其实还是很迷惑的。也就是说,什么时候能触发这些事件然后通知ViewTreeObserver呢?即dispatchOnXxx系列方法系统到底是什么时候调用的呢?只有彻底明白了dispatchOnXxx()执行时机,才能更清楚地使用ViewTreeObserver。

我们知道view的真正绘制流程从ViewRootImpl类中的performTraversals()开始。我只截取相关部分代码说明。

//ViewRootImpl.java

// 第一段代码:
    final View host = mView; //DecorView根布局
    ......
    final View.AttachInfo attachInfo = mAttachInfo;
    ......
    if (mFirst) { //表示第一次被请求执行测量、布局和绘制
        //计算Activity窗口的宽高
        ......
        // 通知视图树已经绑定到它的父窗口上
        ......
        host.dispatchAttachedToWindow(attachInfo, 0);
        attachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        ......
    } else {
        ......
    }

可以看到,如果Activity窗口第一次执行测量、布局和绘制操作,计算完窗口的宽和高之后,就会给mAttachInfo设置一些activity的属性信息,然后通过attachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true)分发通知视图树已经绑定到父窗口。

接下来看第二段代码:

// 第二段代码:
boolean layoutRequested = mLayoutRequested && !mStopped;
    if (layoutRequested) {

        final Resources res = mView.getContext().getResources();

        if (mFirst) {
            // mInTouchMode表示View所处的Window是否处于触摸模式
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            // 确保这个Window的触摸模式已经被设置
            ensureTouchModeLocally(mAddedTouchMode);
        } else {
        ......
        }

进入到ensureTouchModeLocally方法内部,可以看到如果触摸模式发生变化,则会mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged通知出发模式变化的通知。

private boolean ensureTouchModeLocally(boolean inTouchMode) {

    if (mAttachInfo.mInTouchMode == inTouchMode) return false;

    mAttachInfo.mInTouchMode = inTouchMode;
    mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);

    return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
}

接下来看第三段代码。

开始执行measure过程。

//第三段代码
if (!mStopped) { //此窗口的拥有者Activity是否处于停止状态
            //此窗口当前获得焦点的控件是否发生变化
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                 ......
                // 开始执行测量操作
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ......                   
                if (measureAgain) { //是否需要重新测量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
                layoutRequested = true;
            }
        }
    } else {
        ......
    }

第四段代码:执行layout过程。

在layout过程执行结束,则会通过attachInfo.mTreeObserver.dispatchOnGlobalLayout()通知全局布局变化事件。

//第四段代码
final boolean didLayout = layoutRequested && !mStopped;
    boolean triggerGlobalLayoutListener = didLayout
            || attachInfo.mRecomputeGlobalAttributes;
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ......
    }
     ......
    if (triggerGlobalLayoutListener) { 
        attachInfo.mRecomputeGlobalAttributes = false;
        attachInfo.mTreeObserver.dispatchOnGlobalLayout();
    }

第五段代码:执行draw过程。

首先,会通过attachInfo.mTreeObserver.dispatchOnPreDraw()通知观察者绘制过程开始了。如果某一个观察者OnPreDrawListener.onPreDraw返回true,则绘制过程会被取消掉或者重新开始调度。

//第五段代码

boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw() ||
            viewVisibility != View.VISIBLE;  //是否取消绘制
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            ......
            performDraw();
        }
    } else {
        if (viewVisibility == View.VISIBLE) {
            // 重新开始
            scheduleTraversals();
        } ......
    }

在performDraw()->draw()执行过程中,又会涉及到两个事件的分发。如果有发生滚动的话,则会通过attachInfo.mTreeObserver.dispatchOnScrollChanged()通知滚动事件;另外,通过attachInfo.mTreeObserver.dispatchOnDraw()通知观察者绘制的执行。

//ViewRootImpl.java

private void draw(boolean fullRedrawNeeded) {
    ......
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo.mViewScrollChanged) {
        attachInfo.mViewScrollChanged = false;
        attachInfo.mTreeObserver.dispatchOnScrollChanged();
    }

    ......
    attachInfo.mTreeObserver.dispatchOnDraw();
    ......
}

到这里,绘制过程与ViewTreeObserver.dispatchOnXxx方法的调用关系大致就清晰了。还有几个事件这里就不一一说明啦。

七、总结

关于ViewTreeObserver的源码学习就大致就这样,谈了谈基本概念、如何用、还有底层的一些分发机制等。还有很多细节还需要不断深入了解,本文不对之处欢迎大家多多讨论。