探索 Android 多线程 - 2 HandlerThread 与异步消息处理机制

Android 异步的消息处理机制

  • Message
    Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。我们根据它携带的数据得到了一些信息:
    // Message 标识符
    public int what;
    // Message 所携带的信息
    public int arg1;
    public int arg2;
    public Object obj;

    /*package*/ int flags;
    /*package*/ long when;
    /*package*/ Bundle data;
    // 对应的 handle
    /*package*/ Handler target;
    // 回调
    /*package*/ Runnable callback;
    // sometimes we store linked lists of these things
    // 链表的下一个节点
    /*package*/ Message next;
    // 链表表头
    private static Message sPool;
    // 链表容量
    private static int sPoolSize = 0;
    // 链表读写锁
    private static final Object sPoolSync = new Object();

Message 内部使用 obtain() 方法和 recyclerUnchecked() 方法维护了一个 Pool(本质上是一个链表) 来回收和复用 Message 对象。具体的源码如下


 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;
            }
        }
        // 不可复用时直接 alloc
        return new Message();
    }
    
    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 = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
        //插入链表
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }
  • Looper
    Looper 是每个线程中的 MessageQueue 的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当发现 MessageQueue 中存在一条消息,就会将它取出,并传递到 Handler 的handleMessage()方法中。每个线程中也只会有一个Looper对象。

在查看 Looper 源码之前,我们需要先了解 ThreadLocal,这对我们了解 Looper 有非常大的帮助。

并发(2) -- synchronized 与 Lock
带你了解源码中的 ThreadLocal

最简单的解释的话,ThreadLocal (线程本地储存)可以使用相同变量在每个不同的线程都创建不同的存储。有了这个知识储备之后,我们再一窥 Looper 的源码:


   // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static Looper sMainLooper;  // guarded by Looper.class
    final MessageQueue mQueue;
    final Thread mThread;
    
    ...
    
        /* If set, the looper will show a warning log if a message dispatch takes longer than time. */
    private long mSlowDispatchThresholdMs;

在了解主要的变量后,看看这几个维护 sThreadLocal 的方法:

    // 为当前线程设置一个 Looper
    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));
    }
    // 获取当前线程的 Looper
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

以上,就是 Looper.myLooper() 能够在任意线程获取它对应的 Looper 的原理了。然后我们再看看最关键的方法loop():

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    final Looper me = myLooper();
    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();
    }
}

loop() 方法获取当前线程的 MessageQueue,然后通过它的 next() 方法获取队列中的 Message,并把Message 交付给他的 target (即对应的 Handler) 处理。那这个 MessageQueue 又是什么呢?

  • MessageQueue
    MessageQueue 是消息队列的意思,它主要用于存放所有通过 Handler 发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个 MessageQueue 对象。
    每一个队列最重要的当然是他的 offerpoll 方法,而 MessageQueue 对应的就是它的 enqueueMessagenext 方法。我们以这两个方法入手了解这个基本功能:
boolean enqueueMessage(Message msg, long when) {
    // 判断 msg.target 是不是为空
    ...
    
    // 判断 msg.isInUse() 是否为 true
    ...

    synchronized (this) {
        // 判断 MessageQueue 是否 quit 
        ...
        
        // 修改 msg 状态
        msg.markInUse();
        msg.when = when; // 修改它的调用的时间戳
        Message p = mMessages; //mMessages 是当前链表的表头
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // 当前 message 作为表头
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) { // 遍历链表
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;  // 到链表尾部或按时间排序
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            // 将 msg 插入链表
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

enqueueMessage 中可以看到, MessageQueue 实际上维护了一个链表,并且以 Message 的 when 排序。我们再看 next() 方法的源码,了解它如何获取一个 Message,又是如何在 MessageQueue 为空时阻塞的,又是什么时候唤醒的:

 Message next() {
 
 int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis); // native 层使用了 epoll 机制来阻塞一定时间到下次 poll 开始

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null; // 上一个 message
            Message msg = mMessages;// msg 指向链表头节点
            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.
                    //如果还没到 message 执行的时间的话,设置下一次 poll 时间
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next; // 把 msg 从链表中间移出
                    } else {
                        mMessages = msg.next; // 把 msg 从链表表头移出
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }
        ...
        nextPollTimeoutMillis = 0;
    }
 }

在了解其他三个部件的基本工作之后,最后一块零件 Handler 的功能也逐渐清晰了起来。

  • Handler
    Handler 顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用 Handler 的sendMessage() 方法,而发出的消息经过一系列地辗转处理后,最终会传递到 Handler 的 handleMessage() 方法中。使用 Handle 的步骤十分简单:
new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message message) {
        return false;
    }
});

而处理 msg 的逻辑十分清晰

  1. 如果 msg 自带 callback ,则执行自带的 callback,之后执行 4。否者,执行2
  2. 如果Handler 的 Callback 不为空,如构造器中为它赋值,那么执行 3,否者执行4
  3. 如果 Handler.Callback 返回值为 true,直接结束,否者执行4
  4. 执行子类重写的 handleMessage 方法,结束。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) { // msg 有call back 的时候执行 msg 的 callback
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {//执行为 mCallback 设置的方法
                return;
            }
        }
        handleMessage(msg);// 子类实现该方法后,执行使用 overwrite 的方法处理
    }
}

我们在使用 handler.postAtTime() 方法和 handler.postDelayed(),本质上就是 new 一个 runnable 赋值给 msg.callback, 然后在指定的时间调用。而如何定时执行,我么查看源码可知:

// getPostMessage 方法就是使用 Message.obtain() 复用一个 Message 对象, 并为其对应赋值
private static Message getPostMessage(Runnable r, Object token) {
        Message m = Message.obtain();
        m.obj = token;
        m.callback = r;
        return m;
    }
    
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis)
{
    return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
}
    
public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}
    
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;// 当前 looper 对应的 MessageQueue
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis); // 调用 queue 的 enqueueMessage 方法
}

所以我们在每次调用 handler.postDelayed() 时,就是向 handler 所在的线程(如果没有设置 looper 的话就是当前线程)发送一个定时消息。

总结

在了解完整的 Android 的线程通信机制之后,Looper 死循环调用 MessageQueue 的 next() 获取队列中的 Message,并用 Message 对应的 Handler 处理它。

WX20180920-144346.png

拓展:MessageQueue.IdleHandler()

我们先看一下接口的定义:

public static interface IdleHandler {
     /**
     * Called when the message queue has run out of messages and will now
     * wait for more.  Return true to keep your idle handler active, false
     * to have it removed.  This may be called if there are still messages
     * pending in the queue, but they are all scheduled to be dispatched
     * after the current time.
     */
    boolean queueIdle();
}

IdleHandler 在消息队列全部处理完成后调用,它的返回值为true 时,则在处理完之后,再次保留该方法。

IdleHandler 的应用

在 ActivityThread 中就用到了 IdleHandler,为 ActivityThread 添加了一个尝试 GC 的 IdleHandler。

final class GcIdler implements MessageQueue.IdleHandler {
    @Override
    public final boolean queueIdle() {
        doGcIfNeeded();
        return false;
    }
}

在线程阻塞或任务清空后,执行队列中的 IdleHandler 方法,根据它的工作机制,我们可以想象一下它的使用场景。

  • 在 Activity 绘制完成后,执行一个工作。https://wetest.qq.com/lab/view/352.html
  • 在维护某个容器数据的线程中,数据不发生修改的时候,在空闲时,绘制到 UI 上或同步到服务器上。

引用:

【Bugly干货】Android性能优化典范之多线程篇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270