Android开发的Handler消息机制解释

Handler消息机制

如果你想要让一个Android的应用程序反应灵敏,那么你必须防止它的UI线程被阻塞。同样地,将这些阻塞的或者计算密集型的任务转到工作线程去执行也会提高程序的响应灵敏性。然而,这些任务的执行结果通常需要重新更新UI组件的显示,但该操作只能在UI线程中去执行。有一些方法解决了UI线程的阻塞问题,例如阻塞对象,共享内存以及管道技术。Android为了解决这个问题,提供了一种自有的消息传递机制------Handler。Handler是Android Framework架构中的一个基础组件,它实现了一种非阻塞的消息传递机制,在消息转换的过程中,消息的生产者和消费者都不会阻塞。

Handler由以下部分组成:

  • Handler
  • Message
  • MessageQueue
  • Looper

下面我们来了解下它们及它们之间的交互。

(一)、Handler

Handler 是线程间传递消息的即时接口,生产线程和消费线程用以下操作来使用Handler

  • 生产线程:在消息队列中创建、插入或移除消息
  • 消费线程:处理消息

Handler.png

每个Handler都有一个与之关联的Looper和消息队列。有两种创建Handler方式(这里不是说只有两个构造函数,而是说把它的构造函数分为两类)

  • 通过默认的构造方法,使用当前线程中关联的Looper
  • 显式地指定使用Looper

如果没有指定Looper的Handler是无法工作的,因为它无法将消息放到消息队列中。同样地,它无法获取要处理的消息。

public Handler(Callback callback, boolean async) { if (FIND_POTENTIAL_LEAKS) {             final Class<? extends Handler> klass = getClass(); if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && (klass.getModifiers() & Modifier.STATIC) == 0) {                 Log.w(TAG, "The following Handler class should be static or leaks might occur: " +                     klass.getCanonicalName()); } }          mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()"); }         mQueue = mLooper.mQueue;         mCallback = callback;         mAsynchronous = async; }

如果是使用上面Handler的构造函数,它会检查当前线程有没有可用的Looper对象,如果没有,它会抛出一个运行时的异常,如果正常的话,Handler会持有Looper中的消息队列对象的引用。

PS:同一个线程中多的Handler分享一个同样的消息队列,因为他们分享的是同一个Looper对象

Callback参数是一个可选的参数,如果提供的话,它将会处理由Looper分发的过来的消息。

(二)、Message

Message 是容纳任意数据的容器。生产线程发送消息给Handler,Handler将消息加入到消息队列中。消息提供了三种额外的信息,以供Handler和消息队列处理时使用:

  • what:一种标识符,Handler能使用它来区分不同的消息,从而采取不同的处理方法
  • time:告诉消息队列合适处理消息
  • target:表示那一个Handler应该处理消息

android.os.Message 消息一般是通过Handler中以下方法来创建的

public final Message obtainMessage() public final Message obtainMessage(int what) public final Message obtainMessage(int what, Object obj) public final Message obtainMessage(int what, int arg1, int arg2) public final Message obtainMessage(int what, int arg1, int arg2, Object obj)

消息从消息池中获取得到,方法中提供的参数会放到消息体对应的字段中。Handler同样可以设置消息的目标为其自身,这允许我们进行链式调用,比如:

mHandler.obtainMessage(MSG_SHOW_IMAGE, mBitmap).sendToTarget(); 复制

消息池是一个消息对象的单项链表集合,它的最大长度是50。在Handler处理完这条消息之后,消息队列把这个对象返回到消息池中,并且重置其所有字段。

当使用Handler调用post方法来执行一个Runnable时,Handler隐式地创建了一个新的消息,并且设置callback参数来存储这个Runnable。

Message m = Message.obtain(); m.callback = r; 复制

Handler与Message.png

生产线程发送消息给 Handler 的交互

在上图中,我们能看到生产线程和 Handler 的交互。生产者创建了一个消息,并且发送给Handler,随后Handler 将这个消息加入消息队列中,在未来某个时间,Handler 会在消费小城中处理这个消息。

(三)、MessageQueue

MessageQueue是一个消息体对象的无界的单向链表集合,它按照时序将消息插入队列,最小的时间戳将会被首先处理。

MessageQueue.png

消息队列也通过SystemClock.uptimeMillis获取当前时间,维护一个阻塞阀值(dispatch barrier)。当一个消息体的时间戳低于这个值的时候,消息就会分发给Handler进行处理

Handler 提供了三种方式来发送消息:

public final boolean sendMessageDelayed(Message msg, long delayMillis) public final boolean sendMessageAtFrontOfQueue(Message msg) public boolean sendMessageAtTime(Message msg, long uptimeMillis) 复制

以延迟的方式发送消息,是设置了消息体的time字段为SystemClock.uptimeMillis()+delayMillis。然而,通过sendMessageAtFontOfQueue方法是把消息插入到队首,会将其时间字段设置为0,消息会在下一次轮训时被处理。需要谨慎使用这个方法,因为它可能会英系那个消息队列,造成顺序问题,或是其他不可预料的副作用。

(四)、消息队列、Handler、生产线程的交互

现在我们可以概括消息队列、Handler、生产线程的交互:

消息队列、Handler、生产线程的交互.png

上图中,多个生产线程提交消息到不同的Handler中,然而,不同的Handler都与同一个Looper对象关联,因此所有的消息都加入到同一个消息队列中。这一点非常重要,Android中创建的许多不同的Handler都关联到主线程的Looper。

比如:

  • The Choreographer:处理垂直同步与帧更新
  • The ViewRoot:处理输入和窗口时间,配置修改等等
  • The InputMethodManager不会大量生成消息,因为这可能会抑制处理系统

(五)、Looper

Looper 从消息队列中读取消息,然后分发给对应的Handler处理。一旦消息超过阻塞阀,那么Looper就会在下一轮读取过程中读取到它。Looper在没有消息分发的时候变成阻塞状态,当有消息可用时会继续轮询。

每个线程只能关联一个Looper,给线程附加的另外的Looper会导致运行时的异常。通过使用Looper的Threadlocal对象可以保证线程只关联一个Looper对象。

调用Looper.quit()方法会立即终止Looper,并且丢弃消息队列中的已经通过阻塞阀的所有消息。调用Looper.quitSafely()方法能够保证所有待分发的消息在队列中等待的消息被丢弃前得到处理。

Looper.png

Handler 与消息队列和Looper 直接交互的整体流程 Looper 应在线程的run方法中初始化。调用静态方法Looper.prepare()会检查线程是否与一个已存在的Looper关联。这个过程的实现是通过Looper类中的ThreadLocal对象来检查Looper对象是否存在。如果Looper不存在,将会创建一个新的Looper对象和一个新的消息队列。如下代码展示了这个过程

PS: 公有的prepare方法会默认调用prepare(true)

private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); }     sThreadLocal.set(new Looper(quitAllowed)); }

Handler 现在能接收到消息并加入到消息队列中,执行静态方法Looper.loop()方法会开始将消息从消息队列中出队。每次轮训迭代器指向下一条消息,接着分发消息对应目标地的Handler,然后回收消息到消息池中。Looper.looper()方法循环执行这个过程,直到Looper终止。下面代码片段展示这个过程:

public static void loop() { if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); }     final MessageQueue queue = me.mQueue; for (;;) {         Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; }         msg.target.dispatchMessage(msg);         msg.recycleUnchecked(); } }

更多Android开发的技术进阶可以参考《Android核心技术手册》这个文档,;里面记录了1000个技术点板块。可以点击查看详细类目

Handler需要注意的问题

休眠对Handler延时消息的影响

Handler延时消息使用了System.updateMillils()函数,该函数在系统休眠时不会进行计算,那么如果系统进入休眠,例如应用处于后台且锁屏时未连接USB时

只有当再次唤醒时才会执行

Handler的内存泄漏问题

例如组件销毁时,Handler仍然有延时消息没有发送

为了避免这个情况,我们需要在组件销毁的方法中,调用Handler的removeCallbackAndMessage方法,而该方法本质上是调动了MessageQueue的removeCallbackAndMessages方法

该方法的主要作用是遍历当前消息队列中的消息,依次回收到对象池中,并消息队列的头节点置为空,保证消息队列中没有需要执行的消息

 void removeCallbacksAndMessages(Handler h, Object object) {
        if (h == null) {
            return;
        }

        synchronized (this) {
            Message p = mMessages;

            // Remove all messages at front.
            while (p != null && p.target == h
                    && (object == null || p.obj == object)) {
                Message n = p.next;
                mMessages = n;
                // 回收消息
                p.recycleUnchecked();
                p = n;
            }

            // Remove all messages after front.
            while (p != null) {
                Message n = p.next;
                if (n != null) {
                    if (n.target == h && (object == null || n.obj == object)) {
                        Message nn = n.next;
                        n.recycleUnchecked();
                        p.next = nn;
                        continue;
                    }
                }
                p = n;
            }
        }
    }
相关推荐
轻口味43 分钟前
Android应用性能优化
android
全职计算机毕业设计1 小时前
基于 UniApp 平台的学生闲置物品售卖小程序设计与实现
android·uni-app
dgiij1 小时前
AutoX.js向后端传输二进制数据
android·javascript·websocket·node.js·自动化
SevenUUp2 小时前
Android Manifest权限清单
android
高林雨露2 小时前
Android 检测图片抓拍, 聚焦图片后自动完成拍照,未对准图片的提示请将摄像头对准要拍照的图片
android·拍照抓拍
wilanzai2 小时前
Android View 的绘制流程
android
EterNity_TiMe_3 小时前
【Linux基础IO】深入Linux文件描述符与重定向:解锁高效IO操作的秘密
linux·运维·服务器·学习·性能优化·学习方法
INSBUG4 小时前
CVE-2024-21096:MySQLDump提权漏洞分析
android·adb
Mercury Random5 小时前
Qwen 个人笔记
android·笔记
苏苏码不动了5 小时前
Android 如何使用jdk命令给应用/APK重新签名。
android