继上篇内容,本文介绍 ViewTreeObserver 的使用,以及体会其所涉及的观察者模式,期间会附带回顾一些基础知识。最后,我们简单聊一下 Android 的消息传递,附高清示意图,轻松捋清整个传递过程!
在开始下篇之前,有必要回顾一下上篇《解析 ViewTreeObserver 源码,体会观察者模式、Android消息传递(上)》提及的 ViewTreeObserver 的概念:
ViewTreeObserver 是被用来注册监听视图树的观察者,在视图树发生全局改变时将收到通知。这种全局事件包括但不限于:整个视图树的布局发生改变、在视图开始绘制之前、视图触摸模式改变时…
还没有看上篇,或者对上篇已经没印象的,建议先去看一下。
本篇内容较多,为节省篇幅,直接接着上篇继续讲。
#1. 一览 ViewTreeObserver 的大纲
先通过这部分来对类的构成进行粗略的认知,这样才能自如的应对后面的内容。本部分建议大家参考源码去看,这样会更直观、更容易理解,我参考的源码是 Android 6.0 的 SDK(api 23)。
查看类的大纲发现,该类看着挺复杂,但概括起来看就很简单了,下面我们按类别来一个个拿下。(windows 下 AS 查看类大纲的默认快捷键是 Ctrl + F12,大纲模式下还支持搜索以快速定位)
1.1 类的接口
ViewTreeObserver 通过接口回调的方式实现观察者模式,当接收到通知后,通过接口的回调方法告知程序相应的事件发生了。在 ViewTreeObserver 中,包含了 11 个接口,对应着11中观察事件,如下图:
1.2 类的方法
介绍完接口,下面总结一下 ViewTreeObserver 类的方法,大概分为以下四种类型。
添加监听:addOnXxxListener(OnXxxListener)
移除监听:removeOnXxxListener(OnXxxListener)
分发事件:dispatchOnXxx()
其他方法:checkIsAlive()、isAlive()方法等
“其他方法”在上篇差不多提过了,现在我们着重看前三类方法,下面简称 add、remove 和 dispatch 方法。
查看类可知,对于前面那张图所展示的每一个接口,都有与其对应的 add、remove、dispatch 方法。举个例子吧,以 OnGlobalLayoutListener(全局布局监听) 为例,那么与其对应的三类方法就是:
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 在添加该标记时解释道:
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 包下,我们在程序中显然无法访问。
#2. 接口和方法的作用
为了保持内容的连贯和思路的清晰,在上一节只是介绍了 ViewTreeObserver 类的构成,并没有解释具体的作用。下面趁热打铁,看一下各自的作用。此处仍以 OnGlobalLayoutListener(全局布局监听) 接口对应的三个方法为例,其他接口的原理都一样,不再赘述。
2.1 OnGlobalLayoutListener 接口:
注释很精确的概括了其作用:当全局布局状态,或者视图树的子view可见性发生改变时,将调用该回调接口。
该接口包含了一个回调方法 onGlobalLayout(),我们在程序中就是通过覆写该方法,实现自己的逻辑,具体使用将在实战部分介绍。
##2.2 addOnGlobalLayoutListener 和 removeOnGlobalLayoutListener 方法
还是将这俩好基友放在一块介绍,我直接简称 add 和 remove 了。
在程序中,通过 add 方法添加一个对 view 布局发生改变的监听,传入 OnLayoutGlobalListener 接口对象,覆写接口的 onGlobalLayout() 方法,系统会将我们传入的 OnLayoutGlobalListener 存在集合中。
当通过 add 监听之后,我们需要在适当的时候通过 remove 方法移除该监听,防止多次调用。通常在覆写的 onGlobalLayout() 时方法中调用 remove 方法移除监听。
##2.3 dispatchOnGlobalLayout 方法
dispatch 方法一般都由系统调用,我们不需要去关心。在 dispatchOnGlobalLayout 方法中,会遍历存放 OnLayoutGlobalListener 对象的集合,然后调用 OnLayoutGlobalListener 对象的 onGlobalLayout() 方法,通知程序该事件发生了。
[注:上述代码中存放 OnGlobalLayoutListener 的集合 CopyOnWriteArray,值得了解一下,会让你受益匪浅。本打算讲的,但限于篇幅只好作罢,感兴趣的可以上网了解一下]
3.使用姿势(实战)
到目前为止,我们对 ViewTreeObserver 的认识仍停留在概念级别,终于等到了实战环节,验收自己学习成果的时刻到了。
##3.1 使用流程
我们还是先以 OnGlobalLayoutListener 为例介绍一下标准使用流程,这里需要结合上篇所学内容。
- 通过 View 对象的 getViewTreeObserver() 获取 ViewTreeObserver 对象。
- 检测 observer 是否可用。不可用的话再获取一次
- 定义 OnGlobalLayoutListener 接口对象 listener,并覆写 onGlobalLayout() 回调方法。如果只监听一次,记得在方法最后调用 observer.removeOnGlobalLayoutListener() 移除监听,避免重复调用。
- observer.addOnGlobalLayoutListener(listener) ,至此完成对该 View 对象的全局布局监听。
附上一张不完整的流程图,使用在线工具 ProcessOn 画的,挺好用的,推荐给大家:
##3.2 实际使用
上面只是标准使用流程,实际开发中我们不会这么多约束,下面看两个实际的例子。值得注意的是,我们一直所说的 View,实际上指的是 View 及其子类,比如 LinearLayout、ImageView、TextView等。
① 在 onCreate() 中获取 View 的高度
在开发中,我们有时需要在 onCreate() 方法中拿到一个view(任何View的子类)的宽高,这时候我们直接通过 getWidth() 和 getHeight() 方法获取的值均为 0,因为真正的值要在 view 完成 onLayout() 之后才可以返回。这时,我们就可以借助 OnGlobalLayoutListener 监听 view 的布局改变,当 view 布局发生改变且完成 onLayout() 后,就会调用 dispatchOnGlobal() 通知我们,接下来就会走到回调方法 onGlobalLayout() 中去。
view.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//1. do sth you want
width = view.getWidth();
height = view.getHeight;
Log.d("OnGlobalLayoutListener", "width:" + width + ",height:" + height);
//2. remove listener
// api 小于 16
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN){
//使用过时方法
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// api >= 16
else {
//使用替换方法
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
代码已经写得很清楚了,下面再补充两点:
-
因为每次都是通过 getViewTreeObserver() 直接获取 View 当前的observer,所以就没再使用 isAlive() 判断。
-
在介绍 remove 方法时,提到 removeGlobalOnLayoutListener() 方法已经过时,取而代之的是 removeOnGlobalLayoutListener() 方法。后者是在 JELLY_BEAN 版本才引入的,对应的 api 是 16。由于我当前程序的 minSdkVersion 为 14,所以需要根据实际版本号分开处理。其实,在本例中,是不需要分开处理的,我们直接调用已过时的 removeGlobalOnLayoutListener() 方法即可,因为在前面分析过,二者仅仅是名字上的差别。但我之所以这么做,就是为了演示如何判断版本号并据此选择对应的方案。毕竟有些方法系统只提供了高版本的实现,之前的版本就没有对应的方法,此时我们就必须自己实现在低版本上的功能了。
除了 OnGlobalLayoutListener,我们还可以借助 OnPreDrawListener 实现上述功能。同时,OnPreDrawListener 还可以帮助我们实现 View 初始化加载时的动画效果。下面再举个例子,供大家参考以熟悉api,实际的开发中需要灵活运用。
② View 的初始化加载动画
直接上代码,在 onCreate() 方法中:
添加属性动画:
最终效果:
##3.3 补充
在 onCreate() 方法中获取 View 的宽高,网上还提供了重写 Activity 的 onWindowFocusChanged(boolean hasFocus) 方法,或者使用 view.post(Runnable action) 来实现的方案,大家知道就行了,此处不再演示用法。
既然说到了 view.post(Runnable action) ,那就在这儿简单说一下其实现原理,不是很难,但强烈建议对着源码去看::
我们一般会这么使用 post() 方法:
view.post(new Runnable(){
@Override
public void run(){
wid = view.getWidth();
hei = view.getHeight();
}
});
下面,查看 View 的 post(Runnable action) 方法的源码,剖析一下其实现原理:
该方法首先判断 AttachInfo 是否为空,因为其需要 AttachInfo 所持有的 Handler 将传入的 Runnable 发送出去。根据上篇所学,只有在 view 附着到窗口上时,AttachInfo 才会有值,因为我们是在 onCreate() 方法中调用的 post(Runnable action)方法,此时 View 还没有附着到窗口上,所以 AttachInfo 为空。
继续往下执行,系统会将传入的 Runnable 暂存到 ViewRootImpl 的 RunQueue 中,根据源码注释,我们知道这个 RunQueue 负责在 View 还没有持有 Handler 时,将进队操作临时挂起,也就是临时存起来的意思。RunQueue 使用 ArrayList mActions 来管理队列,将 post(Runnable action) 方法传入的 runnable 作为 HandlerAction.action 暂存起来。当 view 在初始化过程中获取到 AttachInfo 后,会在 ViewRootImpl 的 performTraversals() 方法中调用 RunQueue 的 executeActions(Handler handler) 方法,参数传入 mAttachInfo.mHandler。在 executeActions(Handler handler) 方法中,获取之前存入的 runnable 对象,调用 handler.postDelayed(Runnable r, long delay) 将 Runnable 送到全局队列中去,进入 MessageQueue,之后就是取出并调用 runnable 的 run() 方法,这样我们之前在 run() 方法中获取宽高的逻辑就成功调用了。
现在,你应该明白为什么使用 post(Runnable action) 可以拿到 view 的宽高了吧,关键点就在于通过 view.post 传入的 Runnable 在 view 附着到窗口之前,也就是没有 AttachInfo 之前是一直被挂起(缓存)的,直到 view 附着到窗口上才会执行,此时拿到的宽高已经是实际测量后的了。
#4. 观察者模式的体现,知其然知其所以然
前面提到过:ViewTreeObserver 通过接口回调的方式实现观察者模式,那么具体是如何实现的呢?其实这个在前面的内容中已经提及了,现在再串起来说一下:
还记得在上篇说到的观察者模式的三要素:观察者,被观察者,事件。
首先我们通过 view.getViewTreeObserver() 获得的 ViewTreeObserver observer 即为观察者,此时的 view 即为被观察者;然后通过 observer.addXxxListener 添加我们想要观察的事件,比如想要观察 view 的全局布局改变,就是 observer.addOnGlobalLayoutListener(…)。
三要素齐全了,问题是被观察者 view 在发生全局布局改变时如何通知被观察者呢?这时候,不得不说 View 的绘制流程了,这一块没法细说,推荐大家去看《 从ViewRootImpl类分析View绘制的流程》。
暂时不明白绘制流程也没关系,只要知道 view 的绘制过程最终会交给跟视图去管理就可以了,这里的跟视图就是ViewRootImpl。系统通过调用 ViewRootImpl 的 setView(…)方法将要绘制的View传进来,然后在 performTraversals() 方法中开始对 View 进行测量、布局
绘制等操作。
继续上面提出的问题:view 在发生全局布局改变时如何通知被观察者呢?通过分析 ViewRootImpl#performTraversals() 方法的代码,我们在 view 完成 layout(布局) 过程之后,通过调用 dispatchOnGlobalLayout() 方法,通知观察者“全局布局改变”事件发生了。为方便理解,我对源码进行删减并添加了注释(注意圈中部分):
private void performTraversals() {
final View host = mView;
// 省略 其他操作以及 measure 过程
// ...
// 重点看 layout 过程
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
// 若didLayout为true,则对view进行布局
// 同时,上面的triggerGlobalLayoutListener也就为 teue
if (didLayout) {
// 调用 performLayout() 对 view 进行布局
// 感兴趣的自己跟踪,最终会进入到 view 的 onLayout() 方法中
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
//...
}
// 若进行了重新布局,则triggerGlobalLayoutListener 为 teue
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
// 则通过 dispatchOnGlobalLayout() 通知 全局布局发生改变
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
// 省略 其他操作以及 draw 过程
// ...
}
重点:mAttachInfo.mTreeObserver.dispatchOnGlobalLayout()
从代码中可以清晰的看出,当 view 的布局完成后,就通过 dispatchOnGlobalLayout() 方法通知观察者全局事件改变了。在上面分析 dispatchOnGlobalLayout() 方法时,知道该方法依次取出了集合中的 OnGlobalLayoutListener 接口对象,并调用借口的 onGlobalLayout() 方法。
至此,整个观察者模式跑通了。
#5. Android 消息传递
当初在准备写第一篇时,因为中间准备讲一些消息传递的东西,所以就想到了写这个,标题也就带着这个了,后来发现这是个坑。要是说消息传递的原理吧,就不适合公众号定位的读者了,其次说真的,我在这一块还有许多迷糊的的地方,想要整个串起来讲真的做不到,误人子弟就不好了。但是,标题都定了,不能不说啊,要不就是标题党了。在这儿先简单介绍一下消息传递的流程,等后期有时间再尽快补上一篇完整的…
相信许多人都或多或少知道,消息传递离不开 Handler、Looper 和 MessageQueue,下面就围绕这三者说一说消息从创建到进入消息队列、再取出分发的整个过程。但是,在这儿仅仅说一下消息传递的大致流程,对于 Handler 使用细节、Looper 退出(quit) 、MessageQueue的 睡眠和唤醒、进队排序和优先级,以及 ThreadLocal等较深的知识以后再介绍,这次只侧重流程。建议对着源码去看,搭配我根据源码绘制的示意图,很容易理解。
第1步:创建 Looper:
在任何线程中使用 Handler,前提时当前线程存在 Looper 循环,这样我们发送的消息才能取出并分发,否则程序会出错。但是,我们在主线程中使用时,不需要人为创建 Looper 循环,是因为程序启动时,在 ActivityThread # main(String[] args) 方法中,已经默认创建了 Looper 对象,并通过 Looper.loop() 方法启动循环了。现在,先忽略程序默认为主线创建 Looper 循环,我们正常使用 Handler 时,应该先创建 Looper,见下图:
图中相应说明:
-
prepareMainLooper() 方法由系统在创建主线程时,在AvtivityThread # main(String[] args) 方法中调用,我们在程序中不可调用该方法,否则会因重复创建而导致程序崩溃。
-
主线程在调用prepare(boolean quitAllowed)方法时,传入 false,表示主线程的 looper 不可以被退出,因为主线程的 looper 一旦退出,意味着程序就结束了。实际上,looper 的退出取决于对应的 MessageQueue 是否允许退出,这里的 quitAllowed 最终会在新建 Message 时传入,在这儿不究其根本,暂理解为 looper 不允许退出。
感兴趣的可以试试,在程序中的主线程中,调用以下方法:
getMainLooper().quit();
程序将会崩溃并打印如下语句:
Main thread not allowed to quit.
第2步:创建 Handler,启动消息循环(下图)
接着,我们就可以顺利创建我们的 Handler 对象了。在创建 Handler 过程中,会判断当前线程是否存在 Looper 对象,若不存在,会抛出RuntimeException,这也就验证了前面所说的:在没有通过 Looper.prepare() 方法创建 Looper 循环的线程中使用 Handler 会出错。
终于顺利创建了 Handler 对象,现在万事俱备,终于可以发送 Message 到 MessageQueue 了!
且慢!试问现在直接发消息,程序怎么读取并处理呢?其实,在发送之前,我们应该先调用 Looper.loop() 方法来启动消息循环,loop() 方法将调用消息队列 mQueue.next() 方法,从消息队列中取消息。在这里,nexe()方法是一个死循环,将会源源不断的行消息队列中取出消息,如果当前队列消息为空,next()方法会一直阻塞,直到取出消息返回给 loop() 方法。loop() 方法得到 next() 返回的 Message 后,通过 Handler 的 dispatchMessage(Message msg) 方法将获取的消息分发出去。同时,loop() 方法也是一个死循环,在发送一个消息之后,继续去调用mQueue.next()方法等待下一个消息,以此类推…
dispatchMessage(Message msg) 方法不再赘述,就是回调我们的逻辑。
tip:读取消息的过程是死循环,在没有消息时是阻塞的,所以在非主线程中的子线程中,我们使用 Handler 发送并处理完消息之后,应该在子线程中及时通过以下代码停止子线程的 Looper 循环,避免因子线程一直阻塞影响程序性能。
Looper.myLooper().quit();
//或
Looper.myLooper().quitSafely();
第3步:发送消息
接着,我们就可以使用 Handler 发送我们的消息到消息队列了,通过 handler.post(Runnable r) 或 handler.sendMessage(Message msg)方法,我们很容易就可以将一个 Runnable 或者 Message 发送出去(此处仅以常见的API举例)。值得一提的是,使用 post() 方法发送的 Runnable,会被封装在 Message 的 callback 中,最终发送出去的还是 Message 对象。这样一来,无论使用 post() 方法还是 sendMessage() 方法,二者殊途同归,最后会来到 sendMessageAtTime(Message msg, long uptimeMillis) 方法,并执行 enqueue 进队操作,至此消息被成功的存入消息队列。
将第2步和第3步的图放在一起了,如下:
[以上示意图只是给出了一般情况下消息正常传递的过程,并没有考虑完整的消息传递走向,望读者悉知]
#6. 结束
至此,有关 ViewTreeObserver 的源码分析,以及围绕其延伸的诸多知识点均一一介绍完毕。在阅读过程中,如果发现有任何不完善或者有误的地方,还望及时告知,我会及时核实并在以后的文章中更正。
扫描下方二维码,关注我的公众号,及时获取最新文章推送!