[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 消息机制原理的解析。

相关推荐
li_liuliu23 分钟前
Android4.4 在系统中添加自己的System Service
android
C4rpeDime3 小时前
自建MD5解密平台-续
android
鲤籽鲲4 小时前
C# Random 随机数 全面解析
android·java·c#
m0_548514778 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯8 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯8 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐9 小时前
Handle
android
m0_7482329210 小时前
Android Https和WebView
android·网络协议·https
m0_7482517211 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_7482546613 小时前
go官方日志库带色彩格式化
android·开发语言·golang