美文网首页面试Android技术知识Android开发
每日一问:Android 消息机制,我有必要再讲一次!

每日一问:Android 消息机制,我有必要再讲一次!

作者: nanchen2251 | 来源:发表于2019-07-23 19:50 被阅读60次

    坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过。

    我 17 年的 面试系列,曾写过一篇名为:Android 面试(五):探索 Android 的 Handler 的文章,主要讲述的是 Handler 的原理相关面试题,然后简单地给与了一些结论。没想到两年过去,我又开启了 面试系列 的翻版 每日一问 专题,而这一次的卷土重来,只是为了通过源码来探知我们平时可能忽略掉的细节。

    我们在日常开发中,总是不可避免的会用到 Handler,虽说 Handler 机制并不等同于 Android 的消息机制,但 Handler 的消息机制在 Android 开发中早已谙熟于心,非常重要!

    通过本文,你可以非常容易得到一下问题的答案:

    1. HandlerLooperMessageMessageQueue 的原理以及它们之间的关系到底是怎样的?
    2. MessageQueue 存储结构是什么?
    3. 子线程为啥一定要调用 Looper.prepare()Looper.loop()?

    Handler 的简单使用

    相信应该没有人不会使用 Handler 吧?假设在 Activity 中处理一个耗时任务,需要更新 UI,简单看看我们平时是怎么处理的。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main3)
        // 请求网络
        subThread.start()
    }
    
    override fun onDestroy() {
        subThread.interrupt()
        super.onDestroy()
    }
    
    private val handler by lazy(LazyThreadSafetyMode.NONE) { MyHandler() }
    private val subThread by lazy(LazyThreadSafetyMode.NONE) { SubThread(handler) }
    
    private class MyHandler : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            // 主线程处理逻辑,一般这里需要使用弱引用持有 Activity 实例,以免内存泄漏
        }
    }
    
    private class SubThread(val handler: Handler) : Thread() {
        override fun run() {
            super.run()
            // 耗时操作 比如做网络请求
    
            // 网络请求完毕,咱们就得哗哗哗通知 UI 刷新了,直接直接考虑 Handler 处理,其他方案暂时不做考虑
            // 第一种方法,一般这个 data 是请求结果解析的内容
            handler.obtainMessage(1,data).sendToTarget()
            // 第二种方法
            val message = Message.obtain() // 尽量使用 Message.obtain() 初始化
            message.what = 1
            message.obj = data // 一般这个 data 是请求结果解析的内容
            handler.sendMessage(message)
            // 第三种方法
            handler.post(object : Thread() {
                override fun run() {
                    super.run()
                    // 处理更新操作
                }
            })
        }
    }
    

    上述代码非常简单,因为网络请求是一个耗时任务,所以我们新开了一个线程,并在网络请求结束解析完毕后通过 Handler 来通知主线程去更新 UI,简单采用了 3 种方式,细心的小伙伴可能会发现,其实第一种和第二种方法是一样的。就是利用 Handler 来发送了一个携带了内容 Message 对象,值得一提的是:我们应该尽可能地使用 Message.obtain() 而不是 new Message() 进行 Message 的初始化,主要是 Message.obtain() 可以减少内存的申请。

    受到大家在前面文章提出的建议,我们就尽量地少贴一些源码了,大家可以直接很容易发现,上述的所有方法最终都会调用这个方法:

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        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);
    }
    
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    

    上面的代码出现了一个 MessageQueue,并且最终调用了 MessageQueue#enqueueMessage 方法进行消息的入队,我们不得不简单说一下 MessageQueue 的基本情况。

    MessageQueue

    顾名思义,MessageQueue 就是消息队列,即存放多条消息 Message 的容器,它采用的是单向链表数据结构,而非队列。它的 next() 指向链表的下一个 Message 元素。

    boolean enqueueMessage(Message msg, long when) {
        // ... 省略一些检查代码
        synchronized (this) {
            // ... 省略一些检查代码
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // 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.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() 的实现来看,它的主要操作其实就是单链表的插入操作,这里就不做过多的解释了,我们可能应该更多的关心它的出队操作方法 next()

    Message next() {
        // ...
        int nextPollTimeoutMillis = 0;
        for (;;) {
            // ...
            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;
                }
                //...
            }
            //...
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
    

    next() 方法其实很长,不过我们仅仅贴了极少的一部分,可以看到,里面不过是有一个 for (;;) 的无限循环,循环体内部调用了一个 nativePollOnce(long, int) 方法。这是一个 Native 方法,实际作用是通过 Native 层的 MessageQueue 阻塞当前调用栈线程 nextPollTimeoutMillis 毫秒的时间。

    下面是 nextPollTimeoutMillis 取值的不同情况的阻塞表现:

    1. 小于 0,一直阻塞,直到被唤醒;
    2. 等于 0,不会阻塞;
    3. 大于 0,最长阻塞 nextPollTimeoutMillis 毫秒,期间如被唤醒会立即返回。

    可以看到,最开始 nextPollTimeoutMillis 的初始化值是 0,所以不会阻塞,会直接去取 Message 对象,如果没有取到 Message 对象数据,则直接会把 nextPollTimeoutMillis 置为 -1,此时满足小于 0 的条件,会被一直阻塞,直到其他地方调用另外一个 Native 方法 nativeWake(long) 进行唤醒。如果取到值的话,会直接把得到的 Message 对象进行返回。

    原来,nativeWake(long) 方法在前面的 MessageQueue#enqueueMessage 方法有个调用,调用时机是在 MessageQueue 入队消息的过程中。

    现在已经知道:Handler 发送了 Message,消息用 MessageQueue 进行存储,使用 MessageQueue#enqueueMessage 方法进行入队,使用 MessageQueue#next 方法进行轮训消息。这就不免抛出了一个问题,MessageQueue#next 方法是谁调用的?没错,就是 Looper

    Looper

    Looper 在 Android 的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从 MessageQueue 通过 next() 查看是否有新消息,如果有新消息就立刻处理,否则就任由 MessageQueue 阻塞在那里。

    我们直接看看 Looper 最重要的方法: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.");
        }
        // ...
    
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
    
            //...
            try {
                // 分发消息给 handler 处理
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                // ...
            }
            // ...
        }
    }
    

    方法省去了大量的代码,只保留了核心逻辑。可以看到,首先会通过 myLooper() 方法得到 Looper 对象,如果这个 Looper 返回为空的话,则直接抛出异常。否则进入到一个 for (;;) 循环中,调用 MessageQueue#next() 方法进行轮训获取 Message 对象,如果获取的 Message 对象为空,则直接退出 loop() 方法。否则直接通过 msg.target 拿到 Handler 对象,并调用 Handler#dispatchMessage() 方法。

    我们先来看看Handler#dispatchMessage() 方法实现:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    
    private static void handleCallback(Message message) {
        message.callback.run();
    }
    

    代码比较简单,如果 Message 设置了 callback 则,直接调用 message.callback.run(),否则判断是否初始化了 `m

    再来看看 myLooper() 方法:

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
    

    看看 sThreadLocal 是什么:

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    

    这个 ThreadLocal 是什么呢?

    ThreadLocal

    关于 ThreadLocal,我们直接采取 严振杰文章 中的内容。
    看到 ThreadLocal 的第一感觉就是该类和线程有关,确实如此,但是要注意它不是线程,否则它就该叫 LocalThread 了。

    ThreadLocal 是用来存储指定线程的数据的,当某些数据的作用域是该指定线程并且该数据需要贯穿该线程的所有执行过程时就可以使用 ThreadnLocal 存储数据,当某线程使用 ThreadnLocal 存储数据后,只有该线程可以读取到存储的数据,除此线程之外的其他线程是没办法读取到该数据的。

    一些读者看完上面这段话应该还是不理解 ThreadLocal 的作用,我们举个栗子:

    ThreadLocal<Boolean> local = new ThreadLocal<>();
    // 设置初始值为true.
    local.set(true);
    
    Boolean bool = local.get();
    Logger.i("MainThread读取的值为:" + bool);
    
    new Thread() {
        @Override
        public void run() {
            Boolean bool = local.get();
            Logger.i("SubThread读取的值为:" + bool);
    
            // 设置值为false.
            local.set(false);
        }
    }.start():
    
    // 主线程睡1秒,确保上方子线程执行完毕再执行下面的代码。
    Thread.sleep(1000);
    
    Boolean newBool = local.get();
    Logger.i("MainThread读取的新值为:" + newBool);
    

    代码没什么好说的吧,打印出来的日志,你会看到是这样的:

    MainThread读取的值为:true
    SubThread读取的值为:null
    MainThread读取的值为:true
    

    第一条 Log 无可置疑,因为设置了值为 true,因为打印结果没什么好说的。对于第二条 Log,根据上方介绍,某线程使用 ThreadLocal 存储的数据,只能被该线程读取,因此第二条 Log 的结果是:null。紧接着在子线程中设置了 ThreadLocal 的值为 false,然后第三条 Log 将被打印,原理同上,子线程中设置了 ThreadLocal 的值并不影响主线程的数据,所以打印是 true

    实验结果证实:就算是同一个 ThreadLocal 对象,任一线程对其的 set() 和 get() 方法的操作都是相互独立互不影响的。

    Looper.myLooper()

    我们回到 Looper.myLooper()

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    

    我们看看是在哪儿对 sThreadLocal 操作的。

    public static void 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.prepare() 的原因。

    可能你会疑问,我在主线程使用的时候,没有要求 Looper.prepare() 呀。

    原来,我们在 ActivityThread 中,有去显示调用 Looper.prepareMainLooper()

     public static void main(String[] args) {
            // ...
            Looper.prepareMainLooper();
            // ...
            if (sMainThreadHandler == null) {
                sMainThreadHandler = thread.getHandler();
            }
            //...
            Looper.loop();
            // ...
        }
    

    我们看看 Looper.prepareMainLooper()

    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    

    本文参考了:
    《Android 开发艺术探索》
    Android消息机制和应用

    相关文章

      网友评论

        本文标题:每日一问:Android 消息机制,我有必要再讲一次!

        本文链接:https://www.haomeiwen.com/subject/uvlalctx.html