| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> 反思 Android 消息机制的设计与实现 -> 正文阅读 |
|
[移动开发]反思 Android 消息机制的设计与实现 |
上篇文章介绍了 Android 中的 Binder 机制。Binder 在 Android 系统中占有着举足轻重的地位,它是 Android 系统中跨进程通信最重要的方式。而另外一个重要的且能与Binder相提并论的角色便是本文要分析的 Handler。Binder 支撑起了 Android 系统进程间的通信,而 Handler 支撑起的则是进程内线程间的通信。同时,Android 应用程序的运行皆依靠 Handler 的消息机制驱动,这其中就包括触摸事件的分发、View的绘制流程、屏幕的刷新机制以及Activity的生命周期等等。 关于 Handler 其实早在几年前笔者就写过一篇《追根溯源—— 探究Handler的实现原理》的文章。 但是鉴于当时对于 Handler 的理解并不那么深刻,所以这篇文章的内容与网上大多数写 Handler 的文章一样仅仅是源码分析,对 Android 消息机制没有一个深刻的理解和认识。当然,并不是说这样的文章不好,对于初学者来说更适合阅读这样的文章的。所以,如果你对于 Handler 还没有太熟悉的话,不妨先读一读。 如今,作为一个已有多年 Android 开发经验的从业者,在阅读了大量的 framework 源码之后,对于 Android 的消息机制有了一些更加深刻的认识,这是要写这篇文章的原因。 一、从“生产者-消费者”模型说起关注笔者比较久的同学可能看过我之前写过的一篇文章 《深入理解Java线程的等待与唤醒机制》。在这篇文章中为了分析 synchronized 锁的等待与唤醒机制,举了一个 “生产者-消费者” 问题的例子。
可以看得出来,图中的 "生产者"和"消费者"处于两个不同的线程,但是他们共用了同一个队列。生产者在完成数据的生产后会通过 notify 方法唤醒消费者线程,当队列满的时候,生产者线程会调用 wait 方法来阻塞自身。同时,消费者线程在被唤醒后则会从队列中取出数据,并通过 notify 方法唤醒生产者线程继续生产数据。当队列中的数据被取空的时候,消费者线程同样会调用 wait 方法阻塞自身。 关于”生产者-消费者“模型的代码实现就不在这里重复贴出了,大家可以到《深入理解Java线程的等待与唤醒机制》这篇文章中阅读第一章的内容,这些内容对于阅读本文会有一定的帮助。 ”生产者-消费者“模型的案例在平时的开发中是很常见的。例如 Rxjava 的流控制就是典型的”生产者-消费者“模型,除此之外还有线程池的内部实现,以及 Android 系统中输入事件的采集与派发都是基于”生产者-消费者“模型设计的。 上文提到”生产者-消费者“模型解决的是多个线程共享内存的有限缓冲问题。但其实它还解决了另外一个重要问题,即实现了线程间的通信。在生产与消费的过程中由于共用了同一个缓冲队列,”生产者“产生的数据从生产者线程传递给了消费者线程,对缓冲队列内的数据而言就实现了线程的切换。 了解了“生产者-消费者”模型之后对于 Android 理解消息机制的设计思想会有很大的帮助。 二、设计消息机制的框架现在让我们回到 Android 来想一下 Android 中的场景。在文章开头已经提到 Android 应用中触摸事件的分发、View的绘制、屏幕的刷新以及 Activity 的生命周期等都是基于消息实现的。这意味着在 Android 应用中随时都在产生大量的 Message,同时也有大量的 Message 被消费掉。 另外我们都知道在 Android 系统中,UI更新只能在主线程中进行。因此,为了更流畅的页面渲染,所有的耗时操作包括网络请求、文件读写、资源文件的解析等都应该放到子线程中。在这一场景下,线程间的通信就显得尤为重要。因为我们需要在网络请求、文件读写或者资源解析后将得到的数据交给主线程去进行页面渲染。 那在这样的背景下如果让我们作为 Android 系统的设计者,会如何设计并实现 Android 的消息机制,让其即满足具有缓冲功能又能实现线程切换的能力呢? 有了第一章的知识后我想你一定会很自然的想到使用”生产者-消费者“模型来实现。因为这一模型既能解决数据缓冲问题,又实现了数据在线程间的切换。 没错,Android系统的设计者也是这么想的,于是便诞生 Handler 这一杰作!接下来让我们跟随消息机制设计者们的思维来看一下如何实现这一功能。 1.设计消息缓冲区–MessageQueue由于系统中无时无刻都在产生消息,因此我们首先需要有一个消息缓冲区,用来存放各个生产者线程所产生的消息,我们将这个缓冲区命名为 MessageQueue。作为一个消息队列,其内部需要有一个存放消息的容器。同时需要对外提供插入消息和取出消息的接口,将这两个接口方法分别命名为
在 MessageQueue 中维护了一个集合,当插入消息时将消息存入这个集合,取出消息时,将消息从集合中移除并返回。 有了消息队列之后,还需要有”生产者“与”消费者“这两个角色,生产者负责向 MessageQueue 中插入消息,而消费者负责从 MessageQueue 中取出消息并进行消费。 2.设计消息机制的”生产者“–Handler系统各处产生的消息需要存入到消息队列中,等待消费者取出消息并将其消费。因此我们可以设计一个包装类来实现消息插入到消息队列,将这个包装类命名为 Handler。既然作为消息队列的包装, Handler 肯定要持有 MessageQueue,以便于向消息队列中插入消息。同时还需要对外提供插入消息的API,可以将这个插入消息的API命名为 另外,作为Handler的使用方,在通过 Handler 将消息插入到 MessageQueue 后,肯定迫切的需要知道消息何时被消费者处理。因此,还需要在 Handler 中添加一个处理消息的回调方法,以便于使用方重写该方法,并完成需要的逻辑。我们将这个方法命名为
3.设计消息机制的信使-- MessageMessage 应该充当的是信使的作用,即 Message 需要携带调用方赋予的数据。而这一数据类型并不确定,因此我们可以将它声明为 Object 类型。另外,在消息被消费者处理的时候需要通知调用方消息被处理了。因此可以让 Message 持有一个 Handler,以便在消息被处理后回调给Handler。我们将这个 Handler 命名为 target。于是 Message 的伪代码就可以有如下实现:
4.设计消息机制的”消费者“–Looper在完成了消息队列、生产者、以及消息信使的设计之后,我们还需要实现消费者这一角色。作为消费者,它的职责就是从 MessageQueue 中取出 Message ,并将其消费。值得注意的是这个 MessageQueue 必须是与生产者所共用的。这里我们将”消费者“这一角色命名为 Looper。Looper作为”消费者“,其职责就是需要不断的从 MessageQueue 中取出消息并进行消费。那么我们就将这个取消息的方法命名为 loop(),因为需要不断的从 MessageQueue 中取出消息,所以这个方法应该被设计成一个死循环,没有消息的时候就阻塞执行。因此 Looper 的伪代码实现如下:
通过”生产者-消费者“模型,我们可以很轻松的写出 Android 消息机制的大体框架.而接下来我们要思考的是在这个实现过程中会面临什么样的问题,以及该如何去解决。 三、完善消息机制的实现逻辑上一章中,我们搭建起了消息机制的大体框架,下一步就是要实现具体的逻辑了。仔细想一想会发现我们面临着不少的问题,我列举出了以下几个,不妨来思考思考。
1.实现线程切换消息机制一个很重要的需求,即在子线程中获取到的数据需要发送给主线程进行页面渲染。这个让数据从子线程切换到主线程的功能该如何实现呢? 这其实是一个很简单的问题,只需要在主线程中实例化 Looper,同时在实例化 Handler 的时候在 Handler 构造方法中传入 Looper 持有的 MessageQueue 即可。这样 Looper 和 Handler 共享了同一个 MessageQueue。不管 Handler 在哪个线程发送消息,最终 Looper 都会在主线程中取出消息并执行。看一下代码的实现:
当然,这里举的是子线程与主线程的例子,对于子线程与子线程的切换是与之类似的。 2.实现线程级别的 Looper 单例先来看这个问题,一个线程可以有多个 Looper 实例吗?一个线程中有多个 Looper 站在逻辑的角度来看显然是没什么问题的。但是如果站在 Android 系统的角度来考虑,一个线程有多个Looper实例显然有很大的问题!因为 Looper 的 loop() 方法是一个阻塞方法。如果在一个线程中实例化了多个 Looper,并且都调用了它的 loop 方法,那么一定只有第一个调用 loop 方法的 Looper 实例会运行,其他的 Looper 会被阻塞永远也执行不了。 因此,作为消息机制的设计者,我们应该保证单个线程只能实例化一个 Looper。而不能寄托于使用者,要求他们在使用 Looper 的时候只实例化一次。 那么这个时候再来回顾一下上一章我们对 Looper 的设计,似乎是有很大缺陷的。因为此时的Looper可以在主线程中通过 myLooper 方法实例化出任意多个 Looper 对象。显然这是不符合我们的需求的。有的同学说可以将 Looper 设计成单例,这样就不会被实例化出多个 Looper 了。但这样显然也是不符合需求的,我们需要的是在同一个线程里边只能有一个 Looper 实例,但多个线程可以有多个 Looper 实例。也就是说这个 Looper 应该是一个线程级别的单例。那应该怎么实现呢? 说到这里,java基础掌握比较好的同学应该已经想到了,可以使用 ThreadLocal来实现! ThreadLocal提供了线程级别的数据存储能力。即在A线程中使用 ThreadLocal 存储了一个数据,那么这个数据只对A线程可见,只有在A线程中才能取出,其他线程无法取到。关于 ThreadLocal 了解这么多就足够了,这里不再赘述 ThreadLocal 的实现原理,如果你对 ThreadLocal 比较感兴趣可以参考我之前写的一篇文章《Java并发系列番外篇:ThreadLocal原理其实很简单》 有了以上理论的支持,我们就可以重构 Looper 的实现逻辑了。修改后的 Looper 代码如下:
通过以上代码,我们实现了一个线程级别的单例,保证了每个线程只能创建一个 Looper,多次创建就会导致程序崩溃。 3.Message 对象池Android APP 在运行的时候会有大量的 Message 由系统插入到 MessageQueue 中,前面已经提到过的 View 的绘制过程、事件分发过程、Activity 启动过程等等都会向 MessageQueue 插入消息。这就意味着会有大量的 Message 被实例化。如果每次用到 Message 的时候都通过 “new” 关键字来实例化实现 Message 对象,那么肯定会导致严重的内存抖动问题。 因此,为了避免 Message 的频繁实例化,我们可以对获取 Message 对象的过程做一些优化。通常避免频繁的创建对象的解决方案都是使用对象池。也就是维护一个 Message 对象池,在用完之将后将 Message 的数据进行重置,并将其放入到对象池中,等待下次复用。这样就避免了频繁实例化 Message 可能导致的内存抖动问题。主要是了解一下池化思想,这里就直接 copy 系统的源码了,系统源码中 Message 是一个拥有链表结构的类。
可以看到 Message 实现对象池的两个核心方法就是 obtain() 与 recycle(),obtain负责从对象池中取出 Message,如果对象池没有空闲的 Message,则直接实例化 Message,而 recycle() 则是回收用完的Message,并将其插入到复用链表中。这里所谓的对象池其实就是一个空闲的 Message 链表。 4. 无界队列 MessageQueue在上一小节中我们已经知道 Message 其实是一个拥有链表结构的类。因此 MessageQueue 中的容器其实并非像第二章第1小节中写的那样是一个 LinkedList,而是一个 Message 链表。 通常来说”生产者-消费者“模型中的缓冲队列是有特定的容量的,在缓冲队列填满的时候就会阻塞生产者继续添加数据。因此,一个标准的”生产者-消费者“模型必然要考虑背压策略,就比如大家所熟知的 RxJava 由于内部使用的是有界队列,因此当队列的容量不足时就会抛出 但是作为接收系统消息的 MessageQueue 如果被设计成有界队列合适吗?显然是不合适的,因为系统发送的消息多是一些中要的消息,任何事件的丢失都可能会导致严重的系统 bug。所以作为消息机制设计者一定会把 MessageQueue 设计成一个无界队列。这样插入消息永远不会被阻塞,也不用考虑所谓的背压策略了。这是消息机制与标准的 ”生产者-消费者“ 模型的区别之一。 关于 MessageQueue 插入消息与取出消息的实现,前面我们只是简单写了伪代码,而且是使用集合实现的。由于我们已经知道系统源码中的 Message 是一个链表结构的类。因此,我们可以使用链表的结构来实现插消息和取消息。 因为这两个方法的实现还是比较复杂的,因此现在我们跳出设计者的身份,跟随 Android 系统源码来解读这两个方法的实现。 (1)插入消息的实现MessageQueue 插入消息的逻辑是在 enqueueMessage 方法中实现的, 简化后的源码如下:
enqueueMessage 的逻辑其实并不难理解,就是把 Message 插入到链表中去,同时插入链表的位置是根据消息 delay 的时间决定的,delay 的时间越长,插入队列时就越靠后,即越晚执行。最后根据是否要唤醒线程来调用 nativeWake, 这个方法是在 native 层实现的。可以把他理解为"生产者-消费者"模型中 通过 notify 唤醒线程的操作。 (2)取出消息的实现取出消息是通过 MessageQueue 的 next 方法实现的,简化后的 next 方法的源码如下:
这里暂时忽略 next 方法的异步消息处理逻辑以及 IdleHandler 的处理逻辑。简化后的代码并也容易理解。首先是一个用 for 实现的死循环,循环中先调用 nativePollOnce 对线程进行阻塞,这个方法也是一个 native 方法,可以将它理解为”生产者-消费者“模型中阻塞线程的 wait 方法。这个方法的第二个参数表示阻塞的时间,如果是正数,则表示阻塞这个值的毫秒时长,如果是0表示不阻塞,如果小于0,则会一直阻塞。 接下来的逻辑则是判断消息是不是到了该执行的时间了,如果没到,则继续 for 循环执行 nativePollOnce 来阻塞方法,如果消息到了执行的时间就将消息从链表中取出并返回给 Looper,交给 Looper 对消息进行消费。 5.Message 的优先级在标准的”生产者-消费者“模型中消息是没有优先级之分的。即按照标准的队列执行先进先出的逻辑。但是在 Android 消息机制中是需要对消息进行优先级划分的,普通消息应该将优先执行的权利让给那些会影响程序性能的消息,比如 View 绘制的消息、屏幕刷新的消息以及Activity启动的消息等。这是消息机制与标准的”生产者-消费者“模型的又一重要区别。 就我的理解而言,消息机制中的消息一共被划分了四个优先级。优先级由高到低分别是异步消息、普通消息、IDleHandler 以及延迟消息。 (1)异步消息在 Message 的源码中为开发者提供了一个 setAsynchronous 的方法,这个方法是对外开放的。通过这个方法会为消息设置一个异步标记。使用代码如下:
异步消息拥有最高的执行优先级,但是仅仅将其设置为异步消息并没有什么作用。它需要配合同步屏障消息来执行。什么是同步屏障消息呢?其实就是一个 Message.target 为 null 的消息。它的实现逻辑在 MessageQueue 的 postSyncBarrier 方法中。源码如下:
可以看到 postSyncBarrier 方法被
可以看到这里首先判断如果 msg 不为 null,并且 msg.target 为 null,则执行if中的逻辑,而if中则通过 do…while 循环来遍历 message 链表,直到找到异步消息才会终止do…while。然后取出异步消息执行下边的消息处理逻辑。不难看出,当遇到同步屏障消息之后就会阻塞普通消息的执行。然后遍历 Message 链表找到被标记为异步的消息优先执行。 但是有个问题,同步屏障消息是在什么时候被插件 MessageQueue 的呢?答案是在ViewRootImpl 中当计划开始遍历 View 树的时候。看下 ViewRootImpl 的 requestLayout 方法,其源码如下:
可以看到在调用 ViewRootImpl 的 requestLayout 方法后,会执行scheduleTraversals方法,在这个方法中通过 如果你对 Android 的屏幕绘制流程有一定了解的话,应该知道 Vsync 信号与 Choreographer。 Choreographer 会向系统底层订阅 Vsync 信号,系统底层会间隔大约16ms(60hz刷新率的屏幕)发送一次 Vsync信号。等到接收到 Vsync 信号后,Choreographer 会回调 ViewRootImpl 的 doTraversal 方法开始 View 树真正的遍历与绘制。doTraversal 源码如下:
可以看到在doTraversal方法中会将同步屏障消息移除掉,之后普通消息又会得到执行的机会。其实到这里也很容易理解为什么 postSyncBarrier 方法不允许开发者调用了。因为一旦开发者执行这个方法,且没有即使移除同步屏障就会导致普通消息再也没有被执行的机会。 (2)普通消息和延迟消息关于普通消息和延迟消息其实没有什么可说的,普通消息的优先级比异步消息低毋庸置疑。而延迟消息由于在插入消息队列时会根据延迟时间确定插入到队列中的位置,即延迟越久的消息在队列中的位置越靠后。因此延迟消息的优先级是最低的。 (3)IdleHandler除了以上消息的优先级外,还有一种叫 IdelHandler 的消息(这里冒昧的称它为消息),IdelHandler 从本质上来说并不是一个 Message,而是一个接口,其源码如下:
通过注释可以看得出来 queueIdle 方法是在 MessageQueue 中的消息执行完后或者有延迟消息在等待执行时才会被调用。因此可以看得出 IdleHandler 执行的优先级是比异步消息和普通消息低的,但要比延迟消息优先级高。接下来我们看一下IdleHandler的具体源码实现。
可以看到在 MessageQueue 中有一个泛型为 IdleHandler 的 ArrayList 的成员变量,并且提供了addIdleHandler 方法可以向 mIdleHandlers 中添加 IdelHandler,同时也提供了 removeIdleHandler 来移除 IdleHandler。 那接下来继续看 MessageQueue 的 next 方法中对于 IdleHandler 的处理逻辑。
代码中的注释已经写得非常详细了,可以看得出来 IdleHandler 只有在 MessageQueue 没有消息时或者延迟消息没有到执行时间时才会执行 IdleHandler 的 queueIdle 方法。并且 queueIdle 方法的返回值确定了是否会将这个 IdleHandler 从集合中移除。 消息机制的设计者设计 IdelHandler 的目的就是为了执行一些不那么紧急的任务,在异步消息和普通消息执行完后,处于空闲时间时才会开始执行 IdleHandler。 而 IdleHandler 在 Framework 的源码中也是被频繁用到的。典型的用法是 Activity 的 onDestroy 生命周期的调用,就是通过向 MessageQueue 中添加 IdleHandler 来实现的。也就是说当 Activity 执行了 finish 方法后并不会立即执行 onDestory 方法,而是要等到消息队列空闲时 onDestory 才会被调用。 如果是这样的话,在Activity调用 finish 时,不断的向 MessageQueue 中插入消息,是不是会导致 Activity 的 onDestory 一直不会被调用呢?理论上是这样的,但是在系统的源码中做了一个兜底,即如果finish之后过了十秒 Activity 依然没有被销毁则会主动调用 Activity 的 onDestory 来执行销毁逻辑。 关于 onDestory 部分的源码分析可以参考路遥的一篇文章《面试官:为什么 Activity.finish() 之后 10s 才 onDestroy ?》,这里就不做过多解读了。 三、总结本篇文章的内容比较长,文章从 ”生产者-消费者“模型来对比Android消息机制的实现,并尝试站在设计者的角度分析应该怎样设计系统的消息机制,还尝试分析了在实现过程中碰到的问题及解决方案。如果你能细心的看完这篇文章,一定会有所收获,并且会对 Android 的消息机制有一个全新的理解。 推荐阅读《Java并发系列番外篇:ThreadLocal原理其实很简单》 《面试官:为什么 Activity.finish() 之后 10s 才 onDestroy ?》 关于 Handler 其实早在几年前笔者就写过一篇《追根溯源—— 探究Handler的实现原理》的文章。 但是鉴于当时对于 Handler 的理解并不那么深刻,所以这篇文章的内容与网上大多数写 Handler 的文章一样仅仅是源码分析,对 Android 消息机制没有一个深刻的理解和认识。当然,并不是说这样的文章不好,对于初学者来说更适合阅读这样的文章的。所以,如果你对于 Handler 还没有太熟悉的话,不妨先读一读。 如今,作为一个已有多年 Android 开发经验的从业者,在阅读了大量的 framework 源码之后,对于 Android 的消息机制有了一些更加深刻的认识,这是要写这篇文章的原因。 一、从“生产者-消费者”模型说起关注笔者比较久的同学可能看过我之前写过的一篇文章 《深入理解Java线程的等待与唤醒机制》。在这篇文章中为了分析 synchronized 锁的等待与唤醒机制,举了一个 “生产者-消费者” 问题的例子。
可以看得出来,图中的 "生产者"和"消费者"处于两个不同的线程,但是他们共用了同一个队列。生产者在完成数据的生产后会通过 notify 方法唤醒消费者线程,当队列满的时候,生产者线程会调用 wait 方法来阻塞自身。同时,消费者线程在被唤醒后则会从队列中取出数据,并通过 notify 方法唤醒生产者线程继续生产数据。当队列中的数据被取空的时候,消费者线程同样会调用 wait 方法阻塞自身。 关于”生产者-消费者“模型的代码实现就不在这里重复贴出了,大家可以到《深入理解Java线程的等待与唤醒机制》这篇文章中阅读第一章第1小节的内容,这些内容对于阅读本文会有一定的帮助。 ”生产者-消费者“模型的案例在平时的开发中是很常见的。例如 Rxjava 的流控制就是典型的”生产者-消费者“模型,除此之外还有线程池的内部实现,以及 Android 系统中输入事件的采集与派发都是基于”生产者-消费者“模型设计的。 上文提到”生产者-消费者“模型解决的是多个线程共享内存的有限缓冲问题。但其实它还解决了另外一个重要问题,即实现了线程间的通信。在生产与消费的过程中由于共用了同一个缓冲队列,”生产者“产生的数据从生产者线程传递给了消费者线程,对缓冲队列内的数据而言就实现了线程的切换。 了解了“生产者-消费者”模型之后对于 Android 理解消息机制的设计思想会有很大的帮助。 二、设计消息机制的框架现在让我们回到 Android 来想一下 Android 中的场景。在文章开头已经提到 Android 应用中触摸事件的分发、View的绘制、屏幕的刷新以及 Activity 的生命周期等都是基于消息实现的。这意味着在 Android 应用中随时都在产生大量的 Message,同时也有大量的 Message 被消费掉。 另外我们都知道在 Android 系统中,UI更新只能在主线程中进行。因此,为了更流畅的页面渲染,所有的耗时操作包括网络请求、文件读写、资源文件的解析等都应该放到子线程中。在这一场景下,线程间的通信就显得尤为重要。因为我们需要在网络请求、文件读写或者资源解析后将得到的数据交给主线程去进行页面渲染。 那在这样的背景下如果让我们作为 Android 系统的设计者,会如何设计并实现 Android 的消息机制,让其即满足具有缓冲功能又能实现线程切换的能力呢? 有了第一章的知识后我想你一定会很自然的想到使用”生产者-消费者“模型来实现。因为这一模型既能解决数据缓冲问题,又实现了数据在线程间的切换。 没错,Android系统的设计者也是这么想的,于是便诞生 Handler 这一杰作!接下来让我们跟随消息机制设计者们的思维来看一下如何实现这一功能。 1.设计消息缓冲区–MessageQueue由于系统中无时无刻都在产生消息,因此我们首先需要有一个消息缓冲区,用来存放各个生产者线程所产生的消息,我们将这个缓冲区命名为 MessageQueue。作为一个消息队列,其内部需要有一个存放消息的容器。同时需要对外提供插入消息和取出消息的接口,将这两个接口方法分别命名为
在 MessageQueue 中维护了一个集合,当插入消息时将消息存入这个集合,取出消息时,将消息从集合中移除并返回。 有了消息队列之后,还需要有”生产者“与”消费者“这两个角色,生产者负责向 MessageQueue 中插入消息,而消费者负责从 MessageQueue 中取出消息并进行消费。 2.设计消息机制的”生产者“–Handler系统各处产生的消息取消存入到消息队列中,等待消费者取出消息并将其消费。因此我们可以设计一个包装类来实现消息插入到消息队列,将这个包装类命名为 Handler。既然作为消息队列的包装, Handler 肯定要持有 MessageQueue,以便于向消息队列中插入消息。同时还需要对外提供插入消息的API,可以将这个插入消息的API命名为 另外,作为Handler的使用方,在通过 Handler 将消息插入到 MessageQueue 后,肯定迫切的需要知道消息何时被消费者处理。因此,还需要在 Handler 中添加一个处理消息的回调方法,以便于使用方重写该方法,并完成需要的逻辑。我们将这个方法命名为
3.设计消息机制的信使-- MessageMessage 应该充当的是信使的作用,即 Message 需要携带调用方赋予的数据。而这一数据类型并不确定,因此我们可以将它声明为 Object 类型。另外,在消息被消费者处理的时候需要通知调用方消息被处理了。因此可以让 Message 持有一个 Handler,以便在消息被处理后回调给Handler。我们将这个 Handler 命名为 target。于是 Message 的伪代码就可以有如下实现:
4.设计消息机制的”消费者“–Looper在完成了消息队列、生产者、以及消息信使的设计之后,我们还需要实现消费者这一角色。作为消费者,它的职责就是从 MessageQueue 中取出 Message ,并将其消费。值得注意的是这个 MessageQueue 必须是与生产者所共用的。这里我们将”消费者“这一角色命名为 Looper。Looper作为”消费者“,其职责就是需要不断的从 MessageQueue 中取出消息并进行消费。那么我们就将这个取消息的方法命名为 loop(),因为需要不断的从 MessageQueue 中取出消息,所以这个方法应该被设计成一个死循环,没有消息的时候就阻塞执行。因此 Looper 的伪代码实现如下:
通过”生产者-消费者“模型,我们可以很轻松的写出 Android 消息机制的大体框架.而接下来我们要思考的是在这个实现过程中会面临什么样的问题,以及该如何去解决。 三、完善消息机制的实现逻辑上一章中,我们搭建起了消息机制的大体框架,下一步就是要实现具体的逻辑了。仔细想一想会发现我们面临着不少的问题,我列举出了以下几个,不妨来思考思考。
1.实现线程切换消息机制一个很重要的需求,即在子线程中获取到的数据需要发送给主线程进行页面渲染。这个让数据从子线程切换到主线程的功能该如何实现呢? 这其实是一个很简单的问题,只需要在主线程中实例化 Looper,同时在实例化 Handler 的时候在 Handler 构造方法中传入 Looper 持有的 MessageQueue 即可。这样 Looper 和 Handler 共享了同一个 MessageQueue。不管 Handler 在哪个线程发送消息,最终 Looper 都会在主线程中取出消息并执行。看一下代码的实现:
当然,这里举的是子线程与主线程的例子,对于子线程与子线程与子线程的切换是与之类似的。 2.实现线程级别的 Looper 单例先来看这个问题,一个线程可以有多个 Looper 实例吗?一个线程中有多个 Looper 站在逻辑的角度来看显然是没什么问题的。但是如果站在 Android 系统的角度来考虑,一个线程有多个Looper实例显然有很大的问题!因为 Looper 的 loop() 方法是一个阻塞方法。如果在一个线程中实例化了多个 Looper,并且都调用了它的 loop 方法,那么一定只有第一个调用 loop 方法的 Looper 实例会运行,其他的 Looper 会被阻塞永远也执行不了。 因此,作为消息机制的设计者,我们应该保证单个线程只能实例化一个 Looper。而不能寄托与使用者,要求他们在使用 Looper 的时候只实例化一次。 那么这个时候再来回顾一下上一章我们对 Looper 的设计,似乎是有很大缺陷的。因为此时的Looper可以在主线程中通过 myLooper 方法实例化出任意多个 Looper 对象。显然这是不符合我们的需求的。有的同学说可以将 Looper 设计成单例,这样就不会被实例化出多个 Looper 了。但这样显然也是不符合需求的,我们需要的是在同一个线程里边只能有一个 Looper 实例,但多个线程可以有多 个Looper 实例。也就是说这个 Looper 应该是一个线程级别的单例。那应该怎么实现呢? 说到这里,java基础掌握比较好的同学应该已经想到了,可以使用 ThreadLocal来实现! ThreadLocal提供了线程级别的数据存储功能。即在A线程中使用 ThreadLocal 存储了一个数据,那么这个数据只对A线程可见,只有在A线程中才能从取出,其他线程无法取到。关于 ThreadLocal 了解这么多就足够了,这里不再赘述 ThreadLocal 的实现原理,如果你对 ThreadLocal 比较感兴趣可以参考我之前写的一篇文章《Java并发系列番外篇:ThreadLocal原理其实很简单》 有了以上理论的支持,我们就可以重构 Looper 的实现逻辑了。修改后的 Looper 代码如下:
通过以上代码,我们实现了一个线程级别的单例,保证了每个线程只能创建一个 Looper,多次创建就会导致程序崩溃。这样保证了 Looper 在一个线程中只能有一个实例。 3.Message 对象池Android APP 在运行的时候会有大量的 Message 由系统插入到 MessageQueue 中,前面已经提到过的 View的绘制过程、事件分发过程、Activity 启动过程等等都会向 MessageQueue 插入消息。这就意味着会有大量的 Message 被实例化。如果每次用到 Message 的时候都通过 “new” 关键字来实例化实现 Message 对象,那么肯定会导致严重的内存抖动问题。 因此,为了避免 Message 的频繁实例化,我们可以对获取 Message 对象的过程做一些优化。通常避免频繁的创建对象的解决方案都是使用对象池。也就是维护一个 Message 对象池,在用完之将后将 Message 的数据进行重置,并将其放入到对象池中,等待下次复用。这样就避免了频繁实例化 Message 可能导致的内存抖动问题。主要是了解一下池化思想,这里就直接 copy 系统的源码了,系统源码中 Message 是一个拥有链表结构的类。
可以看到 Message 实现对象池的两个核心方法就是 obtain() 与 recycle(),obtain负责从对象池中取出 Message,如果对象池没有空闲的 Message,则直接实例化 Message,而 recycle() 则是回收用完的Message,并将其插入到复用链表中。这里所谓的对象池其实就是一个空闲的 Message 链表。 4. 无界队列 MessageQueue在上一小节中我们已经知道 Message 其实是一个拥有链表结构的类。因此 MessageQueue 中的容器其实并非像第二章第1小节中写的那样是一个 LinkedList,而是一个 Message 链表。 通常来说”生产者-消费者“模型中的缓冲队列是有特定的容量的,在缓冲队列填满的时候就会阻塞生产者继续添加数据。因此,一个标准的”生产者-消费者“模型必然要考虑背压策略,就比如大家所熟知的 RxJava 由于内部使用的是有界队列,因此当队列的容量不足时就会抛出 但是作为接收系统消息的 MessageQueue 如果被设计成有界队列合适吗?显然是不合适的,因为系统发送的消息多是一些中要的消息,任何事件的丢失都可能会导致严重的系统 bug。所以作为消息机制设计者一定会把 MessageQueue 设计成一个无界队列。这样插入消息永远不会被阻塞,也不用考虑所谓的背压策略了。这是消息机制与标准的 ”生产者-消费者“ 模型的区别之一。 关于 MessageQueue 插入消息与取出消息的实现,前面我们只是简单写了伪代码,而且是使用集合实现的。由于我们已经知道系统源码中的 Message 是一个链表结构的类。因此,我们可以使用链表的结构来实现插消息和取消息。 因为这两个方法的实现还是比较复杂的,因此现在我们跳出设计者的身份,跟随 Android 系统源码来解读这两个方法的实现。 (1)插入消息的实现MessageQueue 插入消息的逻辑是在 enqueueMessage 方法中实现的, 简化后的源码如下:
enqueueMessage 的逻辑其实并不难理解,就是把 Message 插入到链表中去,同时插入链表的位置是根据消息 delay 的时间决定的,delay 的时间越长,插入队列时就越靠后,即越晚执行。最后根据是否要唤醒线程来调用 nativeWake, 这个方法是在 native 层实现的。可以把他理解为"生产者-消费者"模型中 通过 notify 唤醒线程的操作。 (2)取出消息的实现取出消息是通过 MessageQueue 的 next 方法实现的,简化后的 next 方法的源码如下:
这里暂时忽略 next 方法的异步消息处理逻辑以及 IdleHandler 的处理逻辑。简化后的代码并也容易理解。首先是一个用 for 实现的死循环,循环中先调用 nativePollOnce 对线程进行阻塞,这个方法也是一个 native 方法,可以将它理解为”生产者-消费者“模型中阻塞线程的 wait 方法。这个方法的第二个参数表示阻塞的时间,如果是正数,则表示阻塞这个值的毫秒时长,如果是0表示不阻塞,如果小于0,则会一直阻塞。 接下来的逻辑则是判断消息是不是到了该执行的时间了,如果没到,则继续 for 循环执行 nativePollOnce 来阻塞方法,如果消息到了执行的时间就将消息从链表中取出并返回给 Looper,交给 Looper 对消息进行消费。 5.Message 的优先级在标准的”生产者-消费者“模型中消息是没有优先级之分的。即按照标准的队列执行先进先出的逻辑。但是在 Android 消息机制中是需要对消息进行优先级划分的,普通消息应该将优先执行的权利让给那些会影响程序性能的消息,比如 View 绘制的消息、屏幕刷新的消息以及Activity启动的消息等。这是消息机制与标准的”生产者-消费者“模型的又一重要区别。 就我的理解而言,消息机制中的消息一共被划分了四个优先级。优先级由高到低分别是异步消息、普通消息、IDleHandler 以及延迟消息。 (1)异步消息在 Message 的源码中为开发者提供了一个 setAsynchronous 的方法,这个方法是对外开放的。通过这个方法会为消息设置一个异步标记。使用代码如下:
异步消息拥有最高的执行优先级,但是仅仅将其设置为异步消息并没有什么作用。它需要配合同步屏障消息来执行。什么是同步屏障消息呢?其实就是一个 Message.target 为 null 的消息。它的实现逻辑在 MessageQueue 的 postSyncBarrier 方法中。源码如下:
可以看到 postSyncBarrier 方法被
可以看到这里首先判断如果 msg 不为 null,并且 msg.target 为 null,则执行if中的逻辑,而if中则通过 do…while 循环来遍历 message 链表,直到找到异步消息才会终止do…while。然后取出异步消息执行下边的消息处理逻辑。不难看出,当遇到同步屏障消息之后就会阻塞普通消息的执行。然后遍历 Message 链表找到被标记为异步的消息优先执行。 但是有个问题,同步屏障消息是在什么时候被插件 MessageQueue 的呢?答案是在ViewRootImpl 中当计划开始遍历 View 树的时候。看下 ViewRootImpl 的 requestLayout 方法,其源码如下:
可以看到在调用 ViewRootImpl 的 requestLayout 方法后,会执行scheduleTraversals方法,在这个方法中通过 如果你对 Android 的屏幕绘制流程有一定了解的话,应该知道 Vsync 信号与 Choreographer。 Choreographer 会向系统底层订阅 Vsync 信号,系统底层会间隔大约16ms(60hz刷新率的屏幕)发送一次 Vsync信号。等到接收到 Vsync 信号后,Choreographer 会回调 ViewRootImpl 的 doTraversal 方法开始 View 树真正的遍历与绘制。doTraversal 源码如下:
可以看到在doTraversal方法中会将同步屏障消息移除掉,之后普通消息又会得到执行的机会。其实到这里也很容易理解为什么 postSyncBarrier 方法不允许开发者调用了。因为一旦开发者执行这个方法,且没有即使移除同步屏障就会导致普通消息再也没有被执行的机会。 (2)普通消息和延迟消息关于普通消息和延迟消息其实没有什么可说的,普通消息的优先级比异步消息低毋庸置疑。而延迟消息由于在插入消息队列时会根据延迟时间确定插入到队列中的位置,即延迟越久的消息在队列中的位置越靠后。因此延迟消息的优先级是最低的。 (3)IdleHandler除了以上消息的优先级外,还有一种叫 IdelHandler 的消息(这里冒昧的称它为消息),IdelHandler 从本质上来说并不是一个 Message,而是一个接口,其源码如下:
通过注释可以看得出来 queueIdle 方法是在 MessageQueue 中的消息执行完后或者有延迟消息在等待执行时才会被调用。因此可以看得出 IdleHandler 执行的优先级是比异步消息和普通消息低的,但要比延迟消息优先级高。接下来我们看一下IdleHandler的具体源码实现。
可以看到在 MessageQueue 中有一个泛型为 IdleHandler 的 ArrayList 的成员变量,并且提供了addIdleHandler 方法可以向 mIdleHandlers 中添加 IdelHandler,同时也提供了 removeIdleHandler 来移除 IdleHandler。 那接下来继续看 MessageQueue 的 next 方法中对于 IdleHandler 的处理逻辑。
代码中的注释已经写得非常详细了,可以看得出来 IdleHandler 只有在 MessageQueue 没有消息时或者延迟消息没有到执行时间时才会执行 IdleHandler 的 queueIdle 方法。并且 queueIdle 方法的返回值确定了是否会将这个 IdleHandler 从集合中移除。 消息机制的设计者设计 IdelHandler 的目的就是为了执行一些不那么紧急的任务,在异步消息和普通消息执行完后,处于空闲时间时才会开始执行 IdleHandler。 而 IdleHandler 在 Framework 的源码中也是被频繁用到的。典型的用法是 Activity 的 onDestroy 生命周期的调用,就是通过向 MessageQueue 中添加 IdleHandler 来实现的。也就是说当 Activity 执行了 finish 方法后并不会立即执行 onDestory 方法,而是要等到消息队列空闲时 onDestory 才会被调用。 如果是这样的话,在Activity调用 finish 时,不断的向 MessageQueue 中插入消息,是不是会导致 Activity 的 onDestory 一直不会被调用呢?理论上是这样的,但是在系统的源码中做了一个兜底,即如果finish之后过了十秒 Activity 依然没有被销毁则会主动调用 Activity 的 onDestory 来执行销毁逻辑。 关于 onDestory 部分的源码分析可以参考路遥的一篇文章《面试官:为什么 Activity.finish() 之后 10s 才 onDestroy ?》,这里就不做过多解读了。 三、总结本篇文章的内容比较长,文章从 ”生产者-消费者“模型来对比Android消息机制的实现,并尝试站在设计者的角度分析应该怎样设计系统的消息机制,还尝试分析了在实现过程中碰到的问题及解决方案。如果你能细心的看完这篇文章,一定会有所收获,并且会对 Android 的消息机制有一个全新的理解。 推荐阅读 |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 3:39:02- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |