[Android] Handler 消息机制原理解析

前言

Handler 消息机制是 Android 中用于处理线程间通信的重要机制。在 Android 开发中,UI 线程(主线程)是一个非常重要的线程,用来处理用户交互、界面更新等操作。但是,如果在 UI 线程中执行耗时操作,就会导致界面卡顿甚至 ANR(Application Not Responding)的情况发生。

为了解决这个问题,Android 提供了 Handler 消息机制,通过它可以实现不同线程之间的通信。Handler 主要用于向特定的线程发送消息,然后在该线程中处理这些消息。通过 Handler,我们可以将耗时操作放到子线程中执行,然后利用 Handler 将结果发送回 UI 线程进行界面更新。

在使用 Handler 消息机制时,通常会涉及到以下几个核心概念:

  1. Handler:用于发送和处理消息的类,可以与 Looper 和 MessageQueue 一起工作来实现消息的处理。
  2. Looper:用于不断地从 MessageQueue 中取出消息,并将其分发给对应的 Handler 进行处理。
  3. MessageQueue:用于存储消息的队列,Handler 发送的消息会被添加到 MessageQueue 中等待处理。
  4. Message:消息的载体,包含了要传递的数据及相关信息。

通过 Handler 消息机制,我们可以实现线程间的通信,方便地在不同线程之间进行数据传递和操作处理,确保 UI 界面的流畅性和响应性。在 Android 开发中,Handler 消息机制被广泛应用于异步任务处理、定时任务调度等场景。

下图是网上流传较为形象的 Handler 消息机制的流程图解: 下面我们将分析上图流程,并通过源码来解析 Handler 消息机制的原理。

1、Handler 处理消息的流程解析

1)发送消息

要处理消息,首先要发送,Handler 类提供了很多发送消息的方法提供给用户根据需求调用,但不论哪个方法,最终都会调用到 Handler 的私有方法:Handler.enqueueMessage() -> MessageQueue.enqueueMessage(),如下图:

这里的 MessageQueue 相当于一个单链表,每个 Message 内部都有一个 Message 类型的成员变量 next,在 MessageQueue.enqueueMessage() 方法中,就是将 MessageQueue 中的最后一个 Message 的 next 变量赋值为当前 Message。

2)接收消息、处理消息

消息发送之后,怎么接收并处理呢?这里就要用到 Looper,我们来从源码看看执行流程:

Java 复制代码
public static void loop() {
    ......
    ......
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}
Java 复制代码
private static boolean loopOnce(final Looper me, final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        return false;
    }
    ......
    ......
    try {
        msg.target.dispatchMessage(msg);
        ......
    } catch (Exception exception) {
        ......
    } finally {
        ......
    }
    ......
    ......
}

来看 me.mQueue.next() 方法:

Java 复制代码
Message next() {
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        ......
        ......
    }
}

这个 next() 方法其实就是从 MessageQueue 中轮询取一个 Message,如果没有就返回 null。如果取到了 Message,那么上述 loopOnce() 方法会继续向下运行,到了 msg.target.dispatchMessage(msg) 这行调用的就是 Handler 的 dispatchMessage() 方法,dispatchMessage() 方法又会回调到 Handler 的 handleMessage() 方法处理消息。

整体流程就是:Looper.loop() -> MessageQueue.next() -> handler.dispatchMessage() -> handler.handleMessage()

2、Handler 的创建

了解完上述流程之后,我们知道 Handler 消息机制中最重要的就是 Looper,如果没有正确使用 Looper,Handler 的功能就不能正常实现,那么如果我们要自定义使用一个 Handler,该怎么做呢?

我们知道 Android 程序的主线程执行入口是 ActivityThread 的 main() 方法,其中就会开启主线程的 Looper,我们在自定义的时候模仿它的行为就好了,来看看这里是怎么做的吧:

Java 复制代码
public static void main(String[] args) {
    ......
    ......
    Looper.prepareMainLooper();
    
    ......
    ......
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    ......
    ......
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

只看创建 Handler 有关的代码,我们发现系统创建主线程的 Handler 主要分三步:

  1. 调用 Looper.prepareMainLooper()
csharp 复制代码
public static void prepareMainLooper() {
   prepare(false);
   synchronized (Looper.class) {
       if (sMainLooper != null) {
           throw new IllegalStateException("The main Looper has already been prepared.");
       }
       sMainLooper = myLooper();
   }
}

可以看到方法里检查了 sMainLooper 是否为空,因此这个方法系统调用了之后我们再调用就会报错,而我们使用自定义 Handler 时需要调用的是 Looper.prepare()

  1. 调用 sMainThreadHandler = thread.getHandler(),这步就是实例化主线程 Handler,那么我们也应当在 prepare() 之后实例化一个 Handler。
  2. 调用 Looper.loop(),最后就再用这个方法启动消息轮询,处理消息。

但是实例化 Handler 的顺序不是固定的,而 Looper.prepare() 方法和 Looper.loop() 方法必须先后执行。

3、Message 的创建

正常情况下,我们需要实例化一个对象都是直接调用构造函数,但是在 Handler 机制中使用 Message 则不能直接创建,原因如下:

拿一块 60Hz 的屏幕来说,每秒钟会刷新 60 次 UI,每次刷新都会发送至少 3 个 Message,分别是:1、确保 UI 能刷新的消息屏障 Message;2、刷新 UI 的 Message;3、移除消息屏障的 Message(这里提到的消息屏障下文会解释)。那么每秒钟就会创建至少 180 个 Message 对象,而对象太多就会占内存,进而导致 GC,而重复创建对象再 GC 的行为,就会导致内存抖动,我们知道每次 GC 都会有一段 STW(Stop The World) 的时间,频繁的 GC 就会导致应用卡顿,因此我们不能通过直接调用构造函数的方式来创建 Message,下面让我们看看该如何来避免上述问题。

首先我们来了解一下 Message 的设计模式:享元设计模式,通过这个设计模式能很好的避免上述问题,和 RecyclerView 的机制类似,Message 也有自己的缓存复用机制 recycle()obtain(),我们来看看这两个方法的源码:

recycle()

Java 复制代码
public void recycle() {
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}
Java 复制代码
void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

回收的主要流程就是将一个 Message 的对象的成员变量初始化,然后判断当前缓存池的大小是否小于缓存池最大容量(50),如果小于就存入缓存池中,如果大于,则缓存池满了,就不再回收。

obtain()

Java 复制代码
public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;

    return m;
}
Java 复制代码
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

复用的流程就是判断缓存池是否为空,不为空则从缓存池中取出一个 Message 并返回,为空则只能 new() 一个 Message 并返回。

我们在使用 Message 的过程中应该通过 Handler 中的 obtainMessage() 方法来获取一个 Message 对象:

Java 复制代码
public final Message obtainMessage()
{
    return Message.obtain(this);
}

这里的调用链是:handler.obtainMessage() -> Message.obtain(Handler h) -> Message.obtain()

4、消息屏障机制

在了解消息屏障机制前,我们首先要知道这里的 Message 主要分为三类:普通 Message、屏障 Message、异步 Message,其中由于消息屏障机制,异步 Message 的执行优先级最高,比如我们上面提到了 UI 刷新的例子,为了不让界面卡顿,那么每次执行 UI 刷新的 Message 都必须立刻执行,但是正常情况下 MessageQueue 中的 Message 执行是按顺序的,因此就将 UI 刷新的 Message 类型设置为异步消息,使得 UI 刷新的 Message 能立刻执行。

消息屏障机制的作用主要体现在以下几个方面:

  1. 控制消息处理顺序: 消息屏障机制可以确保消息队列中的消息按照特定的顺序被处理。这对于一些需要严格控制消息处理顺序的场景非常有用,可以避免并发问题,确保消息的处理符合预期。
  2. 优化性能: 通过消息屏障,可以将一组相关的消息放在一个屏障消息后面,确保它们在一起被处理。这样可以减少消息处理的次数,优化程序的性能。
  3. 避免死锁: 在多线程环境下,如果消息处理的顺序不正确,可能会导致死锁问题。消息屏障机制可以帮助避免这种情况的发生,确保消息的处理顺序是安全的。

下面我们来了解一下消息屏障机制的执行流程。

1) postSyncBarrier() 发送消息屏障 Message

Java 复制代码
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

可以看到,先是通过 obtain() 方法获取了一个 Message,随后在消息队列合适的位置插入这个 Message,那普通 Message 和消息屏障 Message 有什么区别呢?仔细看上面代码,发现并没有给获取到的 Message 的 target 变量赋值(这个 target 变量就是处理这个 Message 的 Handler),也就是说:targetnull 的 Message 就是屏障消息,MessageQueue 就是靠这个变量来区分消息屏障和其他 Message 的。

2) postCallbackDelayedInternal() 发送需要优先处理的异步 Message

发送异步消息最终都会调用到下面这个方法:

Java 复制代码
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }

    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

可以看到在获取消息之后,调用了 msg.setAsynchronous(true) 方法,将标志位设置为 true,然后才发送出去,这样在 next() 方法中就能分辨出异步 Message 并优先处理。

3) 处理消息屏障 Message

还记得我们上文提到的从 MessageQueue 中取出 Message 的 next() 方法吗,不记得的可以翻上去看看,就是那个 for() 循环语句里全部省略没有粘贴上来的那块,

Java 复制代码
for (;;) {
    if (nextPollTimeoutMillis != 0) {
        Binder.flushPendingCommands();
    }

    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        // Try to retrieve the next message.  Return if found.
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }
        if (msg != null) {
            if (now < msg.when) {
                // Next message is not ready.  Set a timeout to wake up when it is ready.
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                // Got a message.
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                msg.markInUse();
                return msg;
            }
        } else {
            // No more messages.
            nextPollTimeoutMillis = -1;
        }

        ......
        ......
}

我们可以看到同步代码块里的第一个 if 语句就判断了当前 Message 的 target 是否为 null,即是否为消息屏障,如果满足条件,则进入 do{} while() 循环找到满足 msg != null && !msg.isAsynchronous() 条件的 Message 并执行,这个 isAsynchronous() 就是判断是否为异步消息的标志,我们下面会介绍。

4) removeSyncBarrier() 移除消息屏障 Message

Java 复制代码
public void removeSyncBarrier(int token) {
    // Remove a sync barrier token from the queue.
    // If the queue is no longer stalled by a barrier then wake it.
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();

        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

这里就是调用了 Message 的 recycleUnchecked() 方法回收了这个 Message。

那么,为什么要移除消息屏障呢?

因为如果不移除的话,根据 next() 方法中的 for() 循环执行逻辑,在有消息屏障的情况下只会处理异步消息,那么普通的消息就不能执行,因此必须移除。

5、IdleHandler

IdleHandler 又是什么东西? 来看👇👇👇

Java 复制代码
public static interface IdleHandler {
    boolean queueIdle();
}

它是一个接口,在 MessageQueue 空闲时,就会执行它的 queueIdle() 方法,我们可以通过下面的方法添加 IdleHandler:

Java 复制代码
Looper.myQueue().addIdleHandler(new IdleHandler(){
    @Override
    public boolean queueIdle(){
    
        // 在这里处理你想做的事,比如打印一个日志
        Log.v("TAG", String.valueOf(System.currentTimeMillis()));
        
        // 这里的返回值决定了是否保留,如果返回 true,则每次空闲都执行,返回 false,则一次空闲执行完之后就被移除了。
        return false; 
    }
});

Kotlin 版本:

kotlin 复制代码
Looper.myQueue().addIdleHandler {
    Log.v("${System.currentTimeMillis()}")
    return true
}

那么这个 queueIdle() 方法在什么时候执行呢?还是在上面提到的 MessageQueue 的 next() 方法中(这么多次提到,可见这个 next() 方法有多重要):

Java 复制代码
for (;;) {
    ......

    synchronized (this) {
        ......
        ......

        if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
            pendingIdleHandlerCount = mIdleHandlers.size();
        }
        if (pendingIdleHandlerCount <= 0) {
            // No idle handlers to run.  Loop and wait some more.
            mBlocked = true;
            continue;
        }

        if (mPendingIdleHandlers == null) {
            mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    // Run the idle handlers.
    // We only ever reach this code block during the first iteration.
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
        final IdleHandler idler = mPendingIdleHandlers[i];
        mPendingIdleHandlers[i] = null; // release the reference to the handler

        boolean keep = false;
        try {
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }

    pendingIdleHandlerCount = 0;

    nextPollTimeoutMillis = 0;
}

可以看到,上面流程是先找到所有的 IdleHandler,遍历执行 queueIdle() 并获取返回值,之后根据返回值判断是否移除这个 IdleHandler。

说了这么多,IdleHandler 一般用在哪呢?用在:

  1. Activity 启动优化:onCreate(),onStart(),onResume() 中耗时较短但非必要的代码可以放到 IdleHandler 中执行,减少启动时间。
  2. 想要一个 View 绘制完成之后,添加其他依附于这个 View 的 View,当然这个用 View 的 post() 方法也能实现,区别就是前者会在 MessageQueue 空闲时执行。
  3. 添加一个返回值为 true 的 IdleHandler,在里面执行方法让某个 View 不停执行某个操作。
  4. 一些三方库中会使用,比如 LeakCanary、Glide 中使用到,用来检测内存泄漏。

👉 以上就是对 Handler 消息机制原理的解析。

相关推荐
太空漫步112 小时前
android社畜模拟器
android
海绵宝宝_5 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存7 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子7 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch11 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391915 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef15 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb