美文网首页安卓学习
一张图学习安卓的消息机制

一张图学习安卓的消息机制

作者: 小怪兽大作战 | 来源:发表于2019-03-03 00:00 被阅读9次

    1 什么是消息机制

    一句话概括消息机制:一个线程持续监听并处理其他线程交给该线程的任务。

    结合实际开发中的例子来说:所有的UI更新操作都应该是在UI线程(ActivityThread)中完成。

    因为安卓的UI操作是线程不安全的。如果并发地对UI进行操作,会产生线程安全问题。即使在并发中加入锁,也会导致某些线程得不到锁而阻塞,降低执行效率;也提高了安卓的学习门槛,对初学者不利。所以,安卓系统将UI操作设计为单线程模式,只有在UI线程才能对UI进行操作。

    如果其他线程需要更新UI,则需要转换到UI线程中。怎么转换到UI线程呢?就需要用到消息机制。

    其他线程通过所持有的UI线程中的handler引用,发出更新UI请求。MessageQueue使用一个单链表将所有请求保存起来。Looper(UI线程中)不断从MessageQueue中读取请求,将请求分发到目标handler中处理。
    在该过程中,handler既是发送请求者也是处理请求者;MessageQueue是存储请求者;Looper不断监听请求,并将请求分发到指定的handler中处理。

    首先我们学习一下安卓消息机制中的几个关键组成

    2 MessageQueue

    消息队列,在内部使用单链表存储着本线程中待执行的所有消息。一个线程只有一个MessageQueue
    。MessageQueue提供插入消息(enqueueMessage(Message,long))和读取消息(Message next())的操作。

    2.1 Boolean enqueueMessage(Message msg,long when)

    boolean enqueueMessage(Message msg, long when) {
            // msg 必须有target也就是必须有handler
            if (msg.target == null) {
                throw new IllegalArgumentException("Message must have a target.");
            }
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }
            //插入消息队列的时候需要做同步,因为会有多个线程同时做往这个队列插入消息
            synchronized (this) {
                if (mQuitting) {
                    IllegalStateException e = new IllegalStateException(
                            msg.target + " sending message to a Handler on a dead thread");
                    Log.w(TAG, e.getMessage(), e);
                    msg.recycle();
                    return false;
                }
    
                msg.markInUse();
                //when 表示这个消息执行的时间,队列是按照消息执行时间排序的
                //如果handler 调用的是postDelay 那么when=SystemClock.uptimeMillis()+delayMillis
                msg.when = when;
                Message p = mMessages;
                boolean needWake;
                if (p == null || when == 0 || when < p.when) {
                    // p==null 表示当前消息队列没有消息
                    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;
                    //将消息放到队列的确切位置,队列是按照msg的when 排序的,链表操作自己看咯
                    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;
                }
    
                // 如果需要唤醒Looper线程,这里调用native的方法实现epoll机制唤醒线程,我们就不在深入探讨了
                if (needWake) {
                    nativeWake(mPtr);
                }
            }
            return true;
        }
    

    enqueueMessage是一个线程同步的方法,首先判断是否需要唤醒主线程(因为主线程会不断读取消息队列中的消息,如果没有消息,主线程会暂停不占有CPU的时间片),如果现在消息队列中没有消息,并且新插入的消息需要立即执行的时候,需要唤醒主线程。然后,根据when将消息插入到链表中的适合位置。

    2.2 Message next()

    next方法是一个无限循环的方法,如果链表中存储有消息,返回链表头部的消息(最先插入的消息),如果链表中没有消息,调用Binder.flushPendingCommands()使线程阻塞。

    Message next() 
           
            final long ptr = mPtr;
            if (ptr == 0) {
                return null;
            }
            int pendingIdleHandlerCount = -1; // -1 only during first iteration
    
            //nextPollTimeoutMillis 表示nativePollOnce方法需要等待nextPollTimeoutMillis 
            //才会返回
            int nextPollTimeoutMillis = 0;
            for (;;) {
                if (nextPollTimeoutMillis != 0) {
                    Binder.flushPendingCommands();
                }
                //读取消息,队里里没有消息有可能会堵塞,两种情况该方法才会返回(代码才能往下执行)
                //一种是等到有消息产生就会返回,
                //另一种是当等了nextPollTimeoutMillis时长后,nativePollOnce也会返回
                nativePollOnce(ptr, nextPollTimeoutMillis);
                //nativePollOnce 返回之后才能往下执行
                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) {
                        // 循环找到一条不是异步而且msg.target不为空的message
                        do {
                            prevMsg = msg;
                            msg = msg.next;
                        } while (msg != null && !msg.isAsynchronous());
                    }
                    if (msg != null) {
                        if (now < msg.when) {
                           // 虽然有消息,但是还没有到运行的时候,像我们经常用的postDelay,
                           //计算出离执行时间还有多久赋值给nextPollTimeoutMillis,
                           //表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回
                            nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                        } else {
                            // 获取到消息
                            mBlocked = false;
                           //链表一些操作,获取msg并且删除该节点 
                            if (prevMsg != null) 
                                prevMsg.next = msg.next;
                            } else {
                                mMessages = msg.next;
                            }
                            msg.next = null;
                            msg.markInUse();
                            //返回拿到的消息
                            return msg;
                        }
                    } else {
                        //没有消息,nextPollTimeoutMillis复位
                        nextPollTimeoutMillis = -1;
                    }
                    .....
                    .....
                  
        }
    

    Looper

    Looper在消息机制中负责消息的分发,Looper会不断调用MessageQueue的next()方法获取消息(这时,线程也切换到了looper线程,完成了线程切换)。如果获得消息,根据该消息的目标handler(handler.target),调用Handler.dispatchMessage(msg)方法将消息分发出去。如果没有获得消息,则会一直阻塞(因为MessageQueue.next方法阻塞,导致looper也会阻塞)。

    public static void loop() {
        final Looper me = myLooper();  //获取TLS存储的Looper对象,获取当前线程的Looper 
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
     
        final MessageQueue queue = me.mQueue;  //获取Looper对象中的消息队列
        ....
    
        for (;;) { //主线程开启无限循环模式
            Message msg = queue.next(); //获取队列中的下一条消息,可能会线程阻塞
            if (msg == null) { //没有消息,则退出循环,退出消息循环,那么你的程序也就可以退出了
                return;
            }
            ....
            //分发Message,msg.target 是一个Handler对象,哪个Handler把这个Message发到队列里,
            //这个Message会持有这个Handler的引用,并放到自己的target变量中,这样就可以回调我们重写
            //的handler的handleMessage方法。
            msg.target.dispatchMessage(msg);
            ....
            ....
            msg.recycleUnchecked();  //将Message回收到消息池,下次要用的时候不需要重新创建,obtain()就可以了。
        }
    }
    

    在Looper的loop方法中,会自动获取该线程的消息队列。
    一个线程只有一个Looper,为了实现每个线程都有自己的Looper实例,安卓中使用了ThreadLocal。ThreadLocal可以为每个线程保存线程私有的对象。主线程的Looper是在ActivityThread.main方法中创建的(在安卓Application的启动中我们学习到了一个App的开始是ActivityThread的main方法,在main方法中会创建ActivityThread的实例,创建Looper,Handler,将自身绑定到AMS中)。

    Handler

    Handler在消息机制中负责消息的发送和消息的处理。分别提供一系列send和post方法来发送消息,dispatchMessage来处理消息。

    发送消息

    Handler虽然提供了很多发送消息的方法,但最后都会调用MessageQueue的enqueueMessage(msg)方法,而该方法就是将消息插入到消息队列中。

    处理消息

    在Looper的分析中我们知道,Looper获取到消息msg后,会调用msg.target.dispatchMessage方法来分发消息。msg.target就是获得该msg的目标handler。通过调用handler的dispatchMessage方法,Handler获得消息,并调用handlerMessage方法来处理消息。这样,就完成了消息的传递和处理。

    ThreadLocal

    经过前面的学习我们知道,一个线程中Looper和MessageQueue只有一个,而同一进程中多个线程是共享内存的,我们如何为每个线程保存一个Looper实例呢?这就用到了ThreadLocal。

    ThreadLocal.set(T value)

        public void set(T value) {
            //获得当前线程
            Thread currentThread = Thread.currentThread();
            //根据当前线程获得Values 对象
            Values values = values(currentThread);
            if (values == null) {
                values = initializeValues(currentThread);
            }
            //根据ThreadLocal对象和需要存储的value将value保存到适合的位置
            values.put(this, value);
        }
    

    当我们调用ThreadLocal.set(T value)方法的时候,会首先获得当前线程,根据线程返回一个Values ,然后调用values.put(this, value);将对象保存到values中。继续看values.put(this, value)方法。

     void put(ThreadLocal<?> key, Object value) {
                cleanUp();
    
                // Keep track of first tombstone. That's where we want to go back
                // and add an entry if necessary.
                int firstTombstone = -1;
    
                for (int index = key.hash & mask;; index = next(index)) {
                    Object k = table[index];
    
                    if (k == key.reference) {
                        // Replace existing entry.
                        table[index + 1] = value;
                        return;
                    }
    
                    if (k == null) {
                        if (firstTombstone == -1) {
                            // Fill in null slot.
                            table[index] = key.reference;
                            table[index + 1] = value;
                            size++;
                            return;
                        }
    
                        // Go back and replace first tombstone.
                        table[firstTombstone] = key.reference;
                        table[firstTombstone + 1] = value;
                        tombstones--;
                        size++;
                        return;
                    }
    
                    // Remember first tombstone.
                    if (firstTombstone == -1 && k == TOMBSTONE) {
                        firstTombstone = index;
                    }
                }
            }
    
    

    put方法中,根据ThreadLocal对象产生一个hash值,然后根据这个hash值将value保存到数组对应的下标中。
    到这里我们知道了ThreadLocal的工作原理:每一个线程中,都有一个对应的Values对象,该对象中有一个数组,用于保存该线程的私有对象。当调用ThreadLocal.put方法的时候,首先会获得当前的线程,根据当前的线程来获得本线程的Values对象。然后根据ThreadLocal对象生成一个Hash值,将需要存储的对象保存到Values内部的数组中hash值对应的下标位置处。

    T get()

        public T get() {
            // Optimized for the fast path.
            //获取当前线程
            Thread currentThread = Thread.currentThread();
            //根据线程来获得Values对象
            Values values = values(currentThread);
            if (values != null) {
                Object[] table = values.table;
                //计算hash值
                int index = hash & values.mask;
                if (this.reference == table[index]) {
                    //返回对应下标的对象
                    return (T) table[index + 1];   
                }
            } else {
                values = initializeValues(currentThread);
            }
    
            return (T) values.getAfterMiss(this);
        }
    

    知道了ThreadLocal的set方法,那么get方法就很容易理解了,我就不再多说了。

    下面用一张图来总结一下。


    image.png

    在该图中,我们创建了两个线程线程1和线程2.在主线程中创建了两个handler:Handler1和Handler2。
    1.线程1持有Handler1的引用,调用Handler1的sendMessage方法将msg1发送出去。Handler1获取本线程(ActivityThread)的MessageQueue,调用MessageQueue的enqueueMessage将msg1添加到链表中。
    2.线程2持有Handler2的引用,调用Handler2的sendMessage方法将msg2发送出去。Handler2获取本线程(ActivityThread)的MessageQueue,调用MessageQueue的enqueueMessage将msg2添加到链表中。
    3.主线程中,Looper不断调用MessageQueue的next方法,获取消息对象。
    4.Looper获得了msg1对象,msg1的target中存储着目标Handler,Looper调用Handler1.dispatchMessage方法将msg1发送到Handler1中。Handler1调用handlerMessage方法来处理msg1。
    5.Looper获得了msg2对象,msg2的target中存储着目标Handler,Looper调用Handler2.dispatchMessage方法将msg2发送到Handler1中。Handler2调用handlerMessage方法来处理msg2。

    注意

    1.我们在自己创建的线程中创建并使用Looper时需要注意,在不适用Looper的时候需要Looper.quit(立即退出)或者Looper.quitSafely(执行完消息队列中所有的消息后才会退出),否则looper会一直阻塞在这里,导致线程不能停止。因为Looper会不断读取MessageQueue的消息,如果读取不到则会阻塞在这里,如果我们不使用的时候不对其停止,Looper会一直阻塞,造成内存泄漏。
    2.ActivityThread的main方法时一个APP的入口。在main方法中,会自动调用Looper.prepareMainLooper()方法来创建主线程的Looper以及MessageQueue。所以在主线程中创建Handler我们不用先创建Looper。但是在其他线程中默认是不创建Looper的,我们需要先创建Looper,然后创建Handler,否则会报错。
    3.主线程(ActivityThread)的Looper在ActivityThread.main方法中创建后,还会创建一个Handler来消息队列进行交互,这个Handler就是H。这个H有着很重要的作用,在ActivityThread将自身绑定到AMS中(通过ApplicationThread),AMS会通过ApplicationThread和ActivityThread通信,发出一系列指令(如:启动acitivity,绑定Application等),这些命令最后都会加入ActivityThread的消息队列中,最终由H来处理。

    相关文章

      网友评论

        本文标题:一张图学习安卓的消息机制

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