解析 ViewTreeObserver 源码,体会观察者模式、Android消息传递(下)

版权声明:本文为博主原创文章,如需转载请联系作者,并显示 注明出处,谢绝私自转载。 https://blog.csdn.net/My_TrueLove/article/details/52653072

继上篇内容,本文介绍 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 为例介绍一下标准使用流程,这里需要结合上篇所学内容。

  1. 通过 View 对象的 getViewTreeObserver() 获取 ViewTreeObserver 对象。
  2. 检测 observer 是否可用。不可用的话再获取一次
  3. 定义 OnGlobalLayoutListener 接口对象 listener,并覆写 onGlobalLayout() 回调方法。如果只监听一次,记得在方法最后调用 observer.removeOnGlobalLayoutListener() 移除监听,避免重复调用。
  4. 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);
                }
            }
        });

代码已经写得很清楚了,下面再补充两点:

  1. 因为每次都是通过 getViewTreeObserver() 直接获取 View 当前的observer,所以就没再使用 isAlive() 判断。

  2. 在介绍 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,见下图:

这里写图片描述

图中相应说明:

  1. prepareMainLooper() 方法由系统在创建主线程时,在AvtivityThread # main(String[] args) 方法中调用,我们在程序中不可调用该方法,否则会因重复创建而导致程序崩溃。

  2. 主线程在调用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 的源码分析,以及围绕其延伸的诸多知识点均一一介绍完毕。在阅读过程中,如果发现有任何不完善或者有误的地方,还望及时告知,我会及时核实并在以后的文章中更正。


扫描下方二维码,关注我的公众号,及时获取最新文章推送!

猜你喜欢

转载自blog.csdn.net/My_TrueLove/article/details/52653072