美文网首页
深入理解Android消息机制(三)原理分析

深入理解Android消息机制(三)原理分析

作者: 怡红快绿 | 来源:发表于2019-05-22 16:03 被阅读0次

    本系列相关阅读


    Android的消息机制实际上就是Handler运行机制,因此本文主要结合源码分析Android消息机制的工作原理,主要内容包括HandlerMessageQueueLooperThreadLocal)。

    一、ThreadLocal的工作原理

    我们首先来回顾一下ThreadLocal的基本概念:一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

    ThreadLocal主要应用于一些特殊的场景中,例如我们熟悉的Looper和ActivityThread中都用到了ThreadLocal。我们都知道,Handler的创建必须依赖当前线程的Looper,也就是说不同的线程都拥有自己特有的Looper,通过ThreadLocal就可以轻松实现Looper在不同线程上的存取。

    为了更直观地理解ThreadLocal的工作原理,接下来通过实际的例子来演示一遍。

    1. 首先定义一个ThreadLocal对象
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    2. 然后分别在不同的线程中设置threadLocal的值,并打印当前线程中的threadLocal值
    new Thread("ThreadA") {
        @Override
        public void run() {
            safeHandler.sendEmptyMessage(0);
            String name = Thread.currentThread().getName();
            threadLocal.set(name);
            Log.d(TAG, name + "#threadLocal = " + threadLocal.get());
        }
    }.start();
    new Thread("ThreadB") {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
    //                threadLocal.set(name);
            Log.d(TAG, name + "#threadLocal = " + threadLocal.get());
        }
    }.start();
    new Thread("ThreadC") {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            threadLocal.set(name);
            Log.d(TAG, name + "#threadLocal = " + threadLocal.get());
        }
    }.start();
    
    3. 查看打印结果
    2019-05-21 11:26:15.088 22766-22784/? D/qianwei: ThreadA#threadLocal = ThreadA
    2019-05-21 11:26:15.094 22766-22786/? D/qianwei: ThreadB#threadLocal = null
    2019-05-21 11:26:15.096 22766-22787/? D/qianwei: ThreadC#threadLocal = ThreadC
    

    从日志可以看出,虽然不同的线程访问的是同一个ThreadLocal对象,但是它们获取到的值确实是互相独立的。那么ThreadLocal是怎么实现这个功能的呢?答案或许就藏在ThreadLocal的set和get方法里面。

    我们接下来分析ThreadLocal的set方法实现:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    

    现在我们知道了,set工作直接交给了一个叫ThreadLocalMap的类,我们继续追踪这个突然出现ThreadLocalMap:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    

    我们可以看到,原来在Thread类内部有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal(实际上key并不是ThreadLocal本身,而是它的一个弱引用),value为代码中放入的值。

    我们继续看ThreadLocalMap的set方法:

    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        // tab是一个Entry环形数组
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            // 找到对应的entry并设置value
            if (k == key) {
                e.value = value;
                return;
            }
            // 替换失效的entry
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    

    本文没有分析ThreadLocalMap细节上的算法实现(感兴趣请移步 ThreadLocal源码解读),但是我们基本上可以得出这样的结论:不同的线程在往同一个ThreadLocal里面写入数据的时候,都是以ThreadLocal的弱引用作为key往自己的ThreadLocalMap里写入;读取过程也是在自己的ThreadLocalMap里找出对应的值,这就是为什么ThreadLocal可以在不同的线程中互不干扰地存储数据。

    二、消息队列的工作原理

    MessageQueue主要包含两个操作:插入和读取,对应的操作方法分别是enqueueMessage和next。enqueueMessage的作用是往消息队列中插入一条消息,next的作用则是在队列中取出一条消息并将消息移除出队列。尽管MessageQueue叫消息队列,但是它的内部实现并不是使用队列,而是使用单链表结构来维护消息列表,这是因为单链表在插入和删除上比较有优势。

    接下来分析一下两个主要方法的实现:enqueueMessage() & next()

    1. enqueueMessage方法

    boolean enqueueMessage(Message msg, long when) {
        //省略部分代码
        synchronized (this) {
            msg.markInUse();
            msg.when = when;    //消息等待时间
            Message p = mMessages;    //队列头部消息
            boolean needWake;
    
            //如果:队列为空 || 当前消息等待时间为0 || 当前消息等待时间小于头部消息等待时间
            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 {
                //唤醒条件:next()方法被阻塞&&(p是消息屏障)消息处理者为null
                //&&是异步消息(这意味着它不受Looper的同步障碍影响)
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) { //循环遍历消息队列,直到:找到等待时间比当前消息短的消息p || 到达队列尾部
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false; //阻塞被解除
                    }
                }
                //msg插入到消息p前面
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
        }
        return true;
    }
    

    其实并不是每一次添加消息时,都会唤醒线程。

    • 当该消息插入到队列头时,会唤醒该线程
    • 当该消息没有插入到队列头,但队列头是屏障,且该消息是队列中 靠前的一个异步消息,则会唤醒线程,执行该消息

    从enqueueMessage的实现来看,它实际上就是单链表的插入操作。

    接下来继续查看next方法是如何实现的:

    2. next方法

    //省略部分代码
    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        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;
                        //前一个消息的next直接指向后一个消息,即当前消息被移除消息队列
                        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;
                }
            }
        }
    }
    

    在消息循环中,如果第一条消息就是屏障消息(target==null),就往后遍历,看看有没有异步消息:

    • 如果没有,则无限休眠,等待被唤醒
    • 如果有,就看离这个消息被触发时间还有多久,设置一个超时时间,继续休眠

    如果消息队列中没有消息,那么循环将会一直阻塞等待被唤醒。next方法是一个典型的单链表删除操作。

    点击了解更多单链表知识 Java实现单链表

    三、Looper的工作原理

    Android的消息机制中,Looper扮演者消息循环的角色。Looper的主要工作就是不停地检查消息队列中是否有新消息:如果消息队列中有新消息,取出消息并交给Handler处理;如果没有消息就一直阻塞在那里。首先看一下Looper的构造方法:

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    

    构造方法负责完成两件事:创建消息队列和保存当前线程对象。我们知道,在线程里面创建Handler需要Looper,否则会报错。如何为线程创建Looper呢?我们只需要执行Looper的prepare方法就可以创建一个Looper,然后调用Looper的loop方法来开启消息循环。

    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            Handler handler = new Handler();
            Looper.loop();
        }
    }).start();
    

    我们注意到Looper提供了两种退出消息循环的方法:quit和quitSafely,两者的区别在于quit会立即退出,而quitSafely只是设置一个退出标记,等消息队列中的消息处理完成以后才安全退出。如果在子线程中手动创建了Looper,那么在所有的任务完成以后应该调用quit方法来终止消息循环,否则这个线程会一直处于等待状态。

    深入理解Android消息机制(二)概述一文中我们简单地介绍过loop方法的实现,我们继续分析Looper是如何终止消息循环的:

    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {  //判断在这之前是否执行过Looper.prepare() 
            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) {
                // 没有消息说明消息队列正在退出
                return;
            }
            try {
                msg.target.dispatchMessage(msg); //把消息交给Handler处理
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            msg.recycleUnchecked();
        }
    }
    
    public void quit() {
        mQueue.quit(false);
    }
    
    public void quitSafely() {
        mQueue.quit(true);
    }
    

    我们可以看到,loop方法内无限循环的唯一结束条件是 MessageQueue的next()方法返回了null。当Looper的quit方法被调用时,实际上是调用MessageQueue的quit方法,当消息队列被标记为退出状态,它的next方法会返回null,因此loop方法成功跳出循环。也就是说,Looper必须要退出,否则loop方法会无限循环下去。这是因为MessageQueue的next是一个阻塞操作,即使消息队列中已经没有新消息要处理,next方法也会一直阻塞,这也就导致loop方法一直阻塞在那里。

    需要注意的是,处理消息的Handler就是发送这条消息的Handler,这样Handler发送的消息最终又交给它的dispatchMessage处理。dispatchMessage方法是在创建Handler时使用的Looper中执行的,而每个Looper都是和线程绑定在一块的,这样就成功地将任务切换到Looper所在的线程中去执行了。

    四、Handler的工作原理

    Handler的主要任务是负责发送消息和处理消息,首先分析Handler的send/post系列消息发送方法和Handler.Callback接口。

    • send系列方法
      send方法在Handler中有多种实现方式供开发者选择。使用这种方法发送消息时,通常需要指定Message内部的what变量,以便接收者在处理消息时根据what判断消息来源。


      send系列方法
    • post系列方法
      和send方法类似,post方法在Handler中也有多种实现方式供开发者选择。Handler调用post方法时会传入一个Runnable参数,然后利用这个参数创建一个Message对象,创建的过程由getPostMessage的方法来完成。我们可以看到getPostMessage方法内有一行代码:< m.callback = r; >,看到这我们应该就明白了, m.callback 就是dispatchMessage方法中的第一个if判断的参与者。Message对象创建完成之后,最终还是需要调用send系列方法来完成消息的发送。

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
    
    post系列方法
    • Handler.Callback接口
      Callback是Handler的一个内部接口,这个接口很简单,只有一个handleMessage方法。Handler最常见的使用方法是:创建一个Handler的子类,在子类中重写handleMessage方法,通过这个子类的构造函数就可以得到我们需要的Handler对象。但是Callback为我们提供了另外一种创建Handler对象的方式:直接使用Handler.Callback作为参数构造出一个Handler对象。我们可以看到构造方法中有一行代码< mCallback = callback; >,这个mCallback就是dispatchMessage方法中的第二个if判断的参与者。
    new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            return false;
        }
    });
    
    public Handler(Callback callback) {
        this(callback, false);
    }
    
    public Handler(Callback callback, boolean async) {
        //省略部分代码
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    
    //这个接口使得我们在创建Handler对象的时候不需要创建Handler的子类
    public interface Callback {
        public boolean handleMessage(Message msg);
    }
    

    当我们使用不同的方式构造Handler对象或使用不同的方法发送消息的时候,在dispatchMessage方法中会采取不同的方式来处理消息:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {   //消息是用post系列方法加入到消息队列中的
            handleCallback(msg);
        } else {//消息是用send系列方法加入到消息队列中的
            if (mCallback != null) {   //用接口作为参数创建Handler对象
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg); //继承Handler类的时候,通常都要重写这个方法
        }
    }
    
    private static void handleCallback(Message message) {
        message.callback.run();
    }
    
    消息处理流程图

    结合代码注释和流程图,我们可以很清楚地看到Handler的消息处理流程,此处就不再重复说明了。

    下图是整个Handler工作过程图:

    图片来源于网络

    参考

    《Android开发艺术探索》
    ThreadLocal源码解读
    Android Handler那些事儿,消息屏障?IdelHandler?ANR?

    相关文章

      网友评论

          本文标题:深入理解Android消息机制(三)原理分析

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