[TOC]

文章参考:https://blog.csdn.net/whoami_I/article/details/102805969

概述

先说ViewTreeObserver是什么?

ViewTreeObserver 是被用来注册监听视图树的观察者,在视图树发生全局改变时将收到通知。本文从 ViewTreeObserver 源码出发,带你剖析 ViewTreeObserver 的设计及使用,并间接体会观察者模式、Android消息传递机制在其中的使用。

我们怎么去理解ViewTreeObserver呢?

ViewTreeObserver,拆开来理解就是 ViewTree 和 Observer。

ViewTree 就是我们常说的视图树,在Android中,所有视图都是由View及View的子类构成,ViewGroup 也是View的子类,它可以装载View或者ViewGroup,这样,一层层嵌套,就有了视图树的概念。

Observer 即观察者的意思,其对应的观察者模式是软件设计模式的一种,在许多编程语言中被广泛运用。其核心在于:一个目标对象,我们称之为被观察者(Observable),管理着所有依附于它的观察者(Observers),当被观察者发生某种改变时(称为事件),被观察者调用对该事件感兴趣的观察者所提供的方法,主动通知观察者。

总结起来就是三要素:观察者,被观察者,事件。

至于在 ViewTreeObserver 中,观察者模式是如何工作的。涉及到 View 的 measure、layout 和 draw 等过程。

ViewTreeObserver说明

ViewTreeObserver 是被用来注册监听视图树的观察者,在视图树发生全局改变时将收到通知。这种全局事件包括但不限于:整个视图树的布局发生改变、在视图开始绘制之前、视图触摸模式改变时…

了解了 ViewTreeObserver 之后,接下来说说如何获取 ViewTreeObserver 对象。我想大多数人会想到 new ViewTreeObserver(),因为这个我们最擅长了。

但是,ViewTreeObserver 的构造方法明确声明了:This constructor should not be called。也就是我们没法调用,当我们尝试这么做时,IDE 会提示:

1
'ViewTreeObserver()' is not public in 'android.view.ViewTreeObserver'.Cannot be accessed from outside package

根据 ViewTreeObserver 类的注释可知,我们只能通过 View 的 getViewTreeObserver() 方法获取 ViewTreeObserver 对象,那么我们看一下 getViewTreeObserver() 方法的源码:

1
2
3
4
5
6
7
8
9
10
 // 代码位于:/frameworks/base/core/java/android/view/View.java
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}

先介绍一下方法中的 AttachInfo,官方文档如此注释说明:

1
A set of information given to a view when it is attached to its parent

意思是:当这个 View 被附着到父窗口时,将会获得这一组信息。说的好抽象啊,还是自己这组信息都有啥吧。

AttachInfo 是 View 类的内部类,找到这个类,浏览一遍,发现其含了一个 ViewTreeObserver 对象,以及该 View 所处的窗口(Window)、设备(Display)、根视图(rootView)等信息,信息量还是蛮大的,感兴趣的可以自己了解下,我们目前只关心这个 ViewTreeObserver 对象。

下面分析方法的逻辑。这个方法首先判断 View 所持有的 mAttachInfo ,当 mAttachInfo 不为空时,直接返回 mAttachInfo 中的 ViewTreeObserver 对象;否则去判断 View 类的 ViewTreeObserver 对象 mFloatingTreeObserver,若 mFloatingTreeObserver 为空,则创建该对象,并返回。

对于 mFloatingTreeObserver,官方注释为:

1
Special tree observer used when mAttachInfo is null.

在View这个类的内部有一个AttachInfo的类,这个类主要是用来记录当整个view树attach到window时的信息,挑几个成员看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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是在哪里赋值的了,发现赋值的地方只有一个

1
2
3
4
5
6
7
8
@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中,然后调用这个方法。具体的我们后面在进行分析。

很明显,这是一个特殊的视图树观察者对象,只有当 mAttachInfo 为空时才会被使用。

至此,我们成功的获取了 ViewTreeObserver 对象。

ViewTreeObserver使用

我们成功获取了 ViewTreeObserver 对象,当我们使用该 对象调用 addOnXxxListener 方法监听 View 的某个状态时,该方法总是首先调用 checkIsAlive() 方法,检测 View 的 ViewTreeObserver 对象是否存活(可用)。

为什么要先去检测呢?官方文档给出的解释是:通过 View 的 getViewTreeObserver() 方法返回的 ViewTreeObserver ,在 View 的生命周期中不能保证始终有效。

既然我如此,那么通过源码我们看一下 checkIsAlive() 都做了什么:

1
2
3
4
5
6
7
 // 代码位于:/frameworks/base/core/java/android/view/View.java
private void checkIsAlive() {
if (!mAlive) {
throw new IllegalStateException(
"This ViewTreeObserver is not alive, call " + "getViewTreeObserver() again");
}
}

逻辑还是很简单的:如果 View 的 ViewTreeObserver 对象不可用,将抛出 IllegalStateException (非法状态异常),并提示我们 重新调用 View 的 getViewTreeObserver() 方法获取对象。

但是,我们能不能在调用 addOnXxxListener 之前,能否检测当前 View 的 ViewTreeObserver 对象是否可用呢,总不能每次等异常了才发现要去重新获取把?答案是肯定的!

ViewTreeObserver 为我们提供了 isAlive() 方法,逻辑很简单,就一句代码“return mAlive”,mAlive 就是在 checkIsAlive() 方法中所判断的变量。该变量标记当前 View 的 ViewTreeObserver 对象是否可用。

ViewTreeObserver 通过接口回调的方式实现观察者模式,当接收到通知后,通过接口的回调方法告知程序相应的事件发生了。在 ViewTreeObserver 中,包含了 11 个接口,对应着11中观察事件,如下图:

image-20210801154316860

介绍完接口,下面总结一下 ViewTreeObserver 类的方法,大概分为以下四种类型。

1
2
3
4
5
6
7
8
// 添加监听:
addOnXxxListener(OnXxxListener)
// 移除监听:
removeOnXxxListener(OnXxxListener)
// 分发事件:
dispatchOnXxx()
// 其他方法:
checkIsAlive()、isAlive()方法等

查看类可知,对于前面那张图所展示的每一个接口,都有与其对应的 add、remove、dispatch 方法。举个例子吧,以 OnGlobalLayoutListener(全局布局监听) 为例,那么与其对应的三类方法就是:

1
2
3
4
5
addOnGlobalLayoutListener(OnGlobalLayoutListener listener);

removeOnGlobalLayoutListener(OnGlobalLayoutListener victim);

dispatchOnGlobalLayout();

这么说,一共有11个接口,那么与之对应的 add、remove、dispatch 方法也就分别有11个,没错,我们通过大纲查看时就是这样。这个大家自行去类中查看,或者根据上面举的例子类推一下,我就不再贴代码了。

虽说 ViewTreeObserver 包含这么多方法,但是系统并没有对我们开放所有的API。我们可以验证一下,在程序代码中先通过 getViewTreeObserver() 获取 View 的 ViewTreeObserver 对象,然后使用该对象分别调用这几类方法,分别模糊匹配 add、remove 和 dispatch,然后查看IDE的智能提示。

先看看调用 add 和 remove 方法:

这里写图片描述

这里写图片描述

如图所示,add 和 remove 方法只分别只有8个,并没有11个。其中remove中最后一个方法removeGloableOnLayoutListener已经过时了,在 API 16 取代它的方法是removeOnGloableLayoutListener。查看removeGloableOnLayoutListener方法可知,其直接调用了removeOnGloableLayoutListener方法,功能上没区别。区别在于名字,肯定是初期方法命名不合理,后来想改,但又不能直接修改或删除。所以,在一开始就设计好一些规范,并在开发过程中按照代码规范开发,是有多重要…

既然都是8个,那各自少掉的3个呢?进 ViewTreeObserver类一看,发现不让外部调用的是与OnWindowShownListener、OnComputeInternalInsetsListener、OnEnterAnimationCompleteListener接口对应的add、remove方法,这几个方法之所以在程序中无法访问,是因为被添加了 @hide标签,这是什么?

@hide 意味着被其标记的方法、类或者变量,在自动生成文档时,将不会出现在API文档中对开发者开放,但是系统可以调用,这就解释了为什么我们只能访问其中8个方法了。其中有些要求对版本有要求,例如添加或移除 OnWindowAttachListener,需要 API 18 以上,而我们一版在开发时会选择最低适配 Android 4.0,也即是 API 为 14,这样一来就无法使用。

其实,可以通过反射访问被 @hide 标记的域。但是不建议这么做,因为 Google 在添加该标记时解释道:

1
We are not yet ready to commit to this API and support it,so @hide。

既然没有准备好提交这个API并支持他,也就意味着 Google 可能会随时修改这些方法(虽然可能性很小),所以出于保险还是不要通过反射使用的好(个人观点)。

再来看看 dispatch 方法可用的有哪些:

这里写图片描述

喔,居然只有3个!查看 ViewTreeObserver 类,发现其余8个不可访问的方法没有声明修饰符,那就是默认的 default 类型。我们知道,default 修饰的方法只能在同一包内可见,ViewTreeObserver.java 在 android.view 包下,我们在程序中显然无法访问。

ViewTreeObserver接口和方法

我们看一下ViewTreeObserver中的接口和方法。看一下各自的作用。此处仍以 OnGlobalLayoutListener(全局布局监听) 接口对应的三个方法为例,其他接口的原理都一样,不再赘述。

OnGlobalLayoutListener 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Interface definition for a callback to be invoked when the global layout
* state or the visibility of views within the view tree changes.
* 当全局布局状态或视图树中视图的可见性发生变化时调用回调的接口定义。
*/
public interface OnGlobalLayoutListener {
/**
* Callback method to be invoked when the global layout state or the visibility
* of views within the view tree changes
*/
public void onGlobalLayout();
}

注释很精确的概括了其作用:当全局布局状态,或者视图树的子view可见性发生改变时,将调用该回调接口。

该接口包含了一个回调方法 onGlobalLayout(),我们在程序中就是通过覆写该方法,实现自己的逻辑,具体使用将在实战部分介绍。

addOnGlobalLayoutListener 和 removeOnGlobalLayoutListener 方法

通过 add 方法添加一个对 view 布局发生改变的监听,传入 OnLayoutGlobalListener 接口对象,覆写接口的 onGlobalLayout() 方法,系统会将我们传入的 OnLayoutGlobalListener 存在集合中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Register a callback to be invoked when the global layout state or the
* visibility of views within the view tree changes
* 注册一个回调,当全局布局状态或视图树中视图的可见性改变时调用
*
* @param listener The callback to add
*
* @throws IllegalStateException If {@link #isAlive()} returns false
*/
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
checkIsAlive();

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

mOnGlobalLayoutListeners.add(listener);
}


当通过 add 监听之后,我们需要在适当的时候通过 remove 方法移除该监听,防止多次调用。通常在覆写的 onGlobalLayout() 时方法中调用 remove 方法移除监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Remove a previously installed global layout callback
*
* @param victim The callback to remove
*
* @throws IllegalStateException If {@link #isAlive()} returns false
*
* @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
*/
public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
return;
}
mOnGlobalLayoutListeners.remove(victim);
}

dispatchOnGlobalLayout 方法

dispatch 方法一般都由系统调用,我们不需要去关心。在 dispatchOnGlobalLayout 方法中,会遍历存放 OnLayoutGlobalListener 对象的集合,然后调用 OnLayoutGlobalListener 对象的 onGlobalLayout() 方法,通知程序该事件发生了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.
// 进行遍历所有的mOnGlobalLayoutListeners然后调用onGlobalLayout
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();
}
}
}

上述代码中存放 OnGlobalLayoutListener 的集合 CopyOnWriteArray,值得了解一下,会让你受益匪浅。本打算讲的,但限于篇幅只好作罢,感兴趣的可以上网了解一下。

到目前为止,我们对 ViewTreeObserver 的认识仍停留在概念级别,终于等到了实战环节,验收自己学习成果的时刻到了。

调用时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 代码位于: framework/base/core/java/android/view/ViewRootImpl.java
* 进行View的遍历
*/
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 开始执行遍历
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
......

// 开始调用mAttachInfo.mTreeObserver.dispatchOnGlobalLayout()
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
.......
// 开始调用mTreeObserver.dispatchOnPreDraw()
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
........
performDraw();
}
}

从上面的流程已经非常清楚整个过程了,首先dcrorview调用dispatchAttachedToWindow,将AttachInfo这个参数一直传递下去,使得整个view树中其实只有一个AttachInfo对象,然后measure、layout,接着就调用我们设置的监听了,因为是在layout之后,所有我们可以得到正确的宽高.