Android中线程间通信原理分析:Looper,MessageQueue,Handler

自问自答的两个问题

在我们去讨论Handler,Looper,MessageQueue的关系之前,我们需要先问两个问题:

1.这一套东西搞出来是为了解决什么问题呢?

2.如果让我们来解决这个问题该怎么做?

以上者两个问题,是我最近总结出来的,在我们学习了解一个新的技术之前,最好是先能回答这两个问题,这样你才能对你正在学习的东西有更深刻的认识。

第一个问题:google的程序员们搞出这一套东西是为了解决什么问题的?这个问题很显而易见,为了解决线程间通信的问题。我们都知道,Android的UI/View这一套系统是运行在主线程的,并且这个主线程是死循环的,来看看具体的证据吧。

public final class ActivityThread {
    public static void main(String[] args) {
        
        //...
        
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}

如上面的代码示例所示,ActivityThread.main()方法作为Android程序的入口,里面我省略了一些初始化的操作,然后就执行了一句Looper.loop()方法,就没了,再下一行就抛异常了。

loop()方法里面实际上就是一个死循环,一直在执行着,不断的从一个MQ(MessageQueue,后面我都缩写成MQ了)去取消息,如果有的话,那么就执行它或者让它的发送者去处理它。

一般来说,主线程循环中都是执行着一些快速的UI操作,当你有手touch屏幕的时候,系统会产生事件,UI会处理这些事件,这些事件都会在主线程中执行,并快速的响应着UI的变化。如果主线程上发生一些比较耗时的操作,那么它后面的方法就无法得到执行了,那么就会出现卡顿,不流畅。

因此,Android并不希望你在主线程去做一些耗时的操作,这里对“耗时”二字进行朴素的理解就行了,就是执行起来需要消耗的时间比较多的操作。比如读写文件,小的文件也许很快,但你无法预料文件的大小,再比如访问网络,再比如你需要做一些复杂的计算等等。

为了不阻碍主线程流畅的执行,我们就必须在需要的时候把耗时的操作放到其他线程上去,当其他线程完成了工作,再给一个通知(或许还带着数据)给到主线程,让主线程去更新UI什么的,当然了,如果你要的耗时操作只是默默无闻的完成就行了,并不需要通知UI,那么你完全不需要给通知给到UI线程。这就是线程间的通信,其他线程做耗时操作,完成了告诉UI线程,让它进行更新。为了解决这个问题,Android系统给我们提供了这样一套方案来解决。

第二个问题:如果让我们来想一套方案来解决这个线程间通信的问题,该怎么做呢?
先看看我们现在已经有的东西,我们有一个一直在循环的主线程,它实现起来大概是这个样子:

public class OurSystem {
    public static void main(String [] args) {
        for (;;) {
            //do something...
        }
    }
}

为什么主线程要一直死循环的执行呢?

关于这一点,我个人并没有特别透彻的认知,但我猜测,对于有GUI的系统/程序,应该都有一个不断循环的主线程,因为这个GUI程序肯定是要跟人进行交互的,也就是说,需要等待用户的输入,比如触碰屏幕,动动鼠标,敲敲键盘什么的,这些事件肯定是硬件层先获得一个响应/信号,然后会不断的向上封装传递。

如果说我们一碰屏幕,一碰鼠标,就开启一个新线程去处理UI上的变化,首先,这当然是可以的!UI在什么线程上更新其实都是可以的嘛,并不是说一定要在主线程上更新,这是系统给我设的一个套子。然后,问题也会复杂的多,如果我们快速的点击2下鼠标,那么一瞬间就开启了两个新线程去执行,那么这两个线程的执行顺序呢?两个独立的线程,我们是无法保证说先启动的先执行。

所以第一个问题就是执行顺序的问题。

第二个问题就是同步,几个相互独立的线程如果要处理同一个资源,那么造成的结果都是令人困惑,不受控制的。另一方面强行给所有的操作加上同步锁,在效率上也会有问题。

为了解决顺序执行的问题,非常容易就想到的一种方案是事件队列,各种各样的事件先进入到一个队列中,然后有个东西会不断的从队列中获取,这样第一个事件一定在第二个事件之前被执行,这样就保证了顺序,如果我们把这个“取事件”的步骤放在一个线程中去做,那么也顺便解决了资源同步的问题。

因此,对于GUI程序会有一个一直循环的(主)线程,可能就是这样来的吧。

这是一个非常纯净的死循环,我们想要做一些事情的话,就得让它从一个队列里面获取一些事情来做,就像打印机一样。因此我们再编写一个消息队列类,来存放消息。消息队列看起来应该是这样:

public class OurMessageQueue() {
    private LinkedList<Message> mQueue = new LinkedList<Message>();
    
    // 放进去一条消息
    public void enQueue() {
        //...
    }
    
    // 取出一条消息
    public Message deQueue() {
        //...
    }
    
    // 判断是否为空队列
    public boolean isEmpty() {
        //...
    }
}

接下来我们的循环就需要改造成能从消息队列里获取消息,并能够根据消息来做些事情了:

public class OurSystem {
    public static void main(String [] args) {
        
        // 初始化消息队列
        OurMessageQueue mq = ...
    
        for (;;) {
            if (!mq.isEmpty()) {
                Message msg = mq.deQueue();
                //do something...
            }
        }
    }
}

现在我们假象一下,我们需要点击一下按钮,然后去下载一个超级大的文件,下载完成后,我们再让主线程显示文件的大小。
首先,按一下按钮,这个事件应该会被触发到主线程来(具体怎么来的我还尚不清楚,但应该是先从硬件开始,然后插入到消息队列中,主线程的循环就能获取到了),然后主线程开启一个新的异步线程来进行下载,下载完成后再通知主线程来更新,代码看上去是这样的:

// 脑补的硬件设备……
public class OurDevice {
    
    // 硬件设备可能有一个回调
    public void onClick() {
    
        // 先拿到同一个消息队列,并把我们要做的事情插入队列中
        OurMessageQueue mq = ...
        Message msg = Message.newInstance("download a big file");
        mq.enQueue(msg);
    }
}

然后,我们的主线程循环获取到了消息:

public class OurSystem {
    public static void main(String [] args) {
        
        // 初始化消息队列
        OurMessageQueue mq = ...
    
        for (;;) {
            if (!mq.isEmpty()) {
                Message msg = mq.deQueue();
                
                // 是一条通知我们下载文件的消息
                if (msg.isDownloadBigFile()) {
                
                    // 开启新线程去下载文件
                    new Thread(new Runnable() {
                        void run() {
                            // download a big file, may cast 1 min...
                            // ...
                            // ok, we finished download task.
                            
                            // 获取到同一个消息队列
                            OurMessageQueue mq = ...
                            
                            // 消息入队
                            mq.enQueue(Message.newInstance("finished download"));
                        }
                    }).start();
                }
                
                // 是一条通知我们下载完成的消息
                if (msg.isFilishedDownload()) {
                    // update UI!
                }
            }
        }
    }
}

注意,主线程循环获取到消息的时候,显示对消息进行的判断分类,不同的消息应该有不同的处理。在我们获取到一个下载文件的消息时,开启了一个新的线程去执行,耗时操作与主线程就被隔离到不同的执行流中,当完成后,新线程中用同一个消息队列发送了一个通知下载完成的消息,主线程循环获取到后,里面就可以更新UI。

这样就是一个我随意脑补的,简单的跨线程通信的方案。

有如下几点是值得注意的:

  • 主线程是死循环的从消息队列中获取消息。
  • 我们要将消息发送到主线程的消息队列,我们需要通过某种方法能获取到主线程的消息队列对象
  • 消息(Message)的结构应该如何设计呢?

Android中的线程间通信方案

Looper

android.os.Looper from Grepcode

Android中有一个Looper对象,顾名思义,直译过来就是循环的意思,Looper也确实干了维持循环的事情。
Looper的代码是非常简单的,去掉注释也就300多行。
在官方文档的注释中,它推荐我们这样来使用它:

class LooperThread extends Thread {
    public Handler mHandler;

    public void run() {
        Looper.prepare();

        mHandler = new Handler() {
            public void handleMessage(Message msg) {
              // process incoming messages here
            }
        };

        Looper.loop();
    }
}

先来看看prepare方法干了什么:

Looper.prepare()

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));
}

注意prepare(boolean)方法中,有一个sThreadLocal变量,这个变量有点像一个哈希表,它的key是当前的线程,也就是说,它可以存储一些数据/引用,这些数据/引用是与当前线程是一一对应的,在这里的作用是,它判断一下当前线程是否有Looper这个对象,如果有,那么就报错了,"Only one Looper may be created per thread",一个线程只允许创建一个Looper,如果没有,就new一个新的塞进这个哈希表中。然后它调用了Looper的构造方法。

Looper的构造方法

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

Looper的构造方法中,很关键的一句,它new了一个MessageQueue对象,并自己维持了这个MQ的引用。

此时prepare()方法的工作就结束了,接下来需要调用静态方法loop()来启动循环。

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.");
    }
    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);

        //...
    }
}

loop()方法,我做了省略,省去了一些不关心的部分。剩下的部分非常的清楚了,首先调用了静态方法myLooper()获取一个Looper对象。

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

myLooper()同样是静态方法,它是直接从这个ThreadLocal中去获取,这个刚刚说过了,它就类似于一个哈希表,key是当前线程,因为刚刚prepare()的时候,已经往里面set了一个Looper,那么此时应该是可以get到的。拿到当前线程的Looper后,接下来,final MessageQueue queue = me.mQueue;拿到与这个Looper对应的MQ,拿到了MQ后,就开启了死循环,对消息队列进行不停的获取,当获取到一个消息后,它调用了Message.target.dispatchMessage()方法来对消息进行处理。

Looper的代码看完了,我们得到了几个信息:

  • Looper调用静态方法prepare()来进行初始化,一个线程只能创建一个与之对应的LooperLooper初始化的时候会创建一个MQ,因此,有了这样的对应关系,一个线程对应一个Looper,一个Looper对应一个MQ。可以说,它们三个是在一条线上的。
  • Looper调用静态方法loop()开始无限循环的取消息,MQ调用next()方法来获取消息

MessageQueue

android.os.MessageQueue from Grepcode

对于MQ的源码,简单的看一下,构造函数与next()方法就好了。

MQ的构造方法

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

MQ的构造方法简单的调用了nativeInit()来进行初始化,这是一个jni方法,也就是说,可能是在JNI层维持了它这个消息队列的对象。

MessageQueue.next()

Message next() {
    
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int nextPollTimeoutMillis = 0;
    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 (false) Log.v("MessageQueue", "Returning message: " + msg);
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
        }

    }
}

next()方法的代码有些长,我作了一些省略,请注意到,这个方法也有一个死循环,这样做的效果就是,在Looper的死循环中,调用了next(),而next()这里也在死循环,表面上看起来,方法就阻塞在Looper的死循环中的那一行了,知道next()方法能返回一个Message对象出来。

简单浏览MQ的代码,我们得到了这些信息:

  • MQ的初始化是交给JNI去做的
  • MQ的next()方法是个死循环,在不停的访问MQ,从中获取消息出来返回给Looper去处理。

Message

android.os.Message from Grepcode

Message对象是MQ中队列的element,也是Handler发送,接收处理的一个对象。对于它,我们需要了解它的几个成员属性即可。

Message的成员变量可以分为三个部分:

  • 数据部分:它包括what(int), arg1(int), arg2(int), obj(Object), data(Bundle)等,一般用这些来传递数据。
  • 发送者(target):它有一个成员变量叫target,它的类型是Handler的,这个成员变量很重要,它标记了这个Message对象本身是谁发送的,最终也会交给谁去处理。
  • callback:它有一个成员变量叫callback,它的类型是Runnable,可以理解为一个可以被执行的代码片段。

Handler

android.os.Handler from Grepcode

Handler对象是在API层面供给开发者使用最多的一个类,我们主要通过这个类来进行发送消息与处理消息。

Handler的构造方法(初始化)

通常我们调用没有参数的构造方法来进行初始化,使用起来大概是这样的:

Handler mHandler = new Handler() {
    handleMessage(Message msg) {
        //...
    }
}

没有参数的构造方法最终调用了一个两个参数的构造方法,它的部分源码如下:

public Handler(Callback callback, boolean async) {
    //...
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

注意到,它对mLooper成员变量进行了赋值,通过Looper.myLooper()方法获取到当前线程对应的Looper对象。上面已经提到过,如果Looper调用过prepare()方法,那么这个线程对应了一个Looper实例,这个Looper实例也对应了一个MQ,它们三者之间是一一对应的关系。

然后它通过mLooper对象,获取了一个MQ,存在自己的mQueue成员变量中。

Handler的初始化代码说明了一点,Handler所初始化的地方(所在的线程),就是从将这个线程对应的Looper的引用赋值给Handler,让Handler也持有。

对于主线程来说,我们在主线程的执行流中,new一个Handler对象,Handler对象都是持有主线程的Looper(也就是Main Looper)对象的。

同样的,如果我们在一个新线程,不调用Looper.prepare()方法去启动一个Looper,直接new一个Handler对象,那么它就会报错。像这样

new Thread(new Runnable() {
        @Override
        public void run() {
            //Looper.prepare(); 

            //因为Looper没有初始化,所以Looper.myLooper()不能获取到一个Looper对象
            Handler h = new Handler();
            h.sendEmptyMessage(112);

        }
     }).start();

以上代码运行后会报错:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

小结Handler的初始化会获取到当前线程的Looper对象,并通过Looper拿到对应的MQ对象,如果当前线程的执行流并没有执行过Looper.prepare(),则无法创建Handler对象。

Handler.sendMessage()

sendMessage消息有各种各样的形式或重载,最终会调用到这个方法:

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);
}

它又调用了enqueueMessage方法:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

注意到它对Messagetarget属性进行了赋值,这样这条消息就知道自己是被谁发送的了。然后将消息加入到队列中。

Handler.dispatchMessage()

Message对象进入了MQ后,很快的会被MQ的next()方法获取到,这样Looper的死循环中就能得到一个Message对象,回顾一下,接下来,就调用了Message.target.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();
}

public void handleMessage(Message msg) {
    //这个方法是空实现,让客户端程序员去覆写实现自己的逻辑
}

dispatchMessage方法有两个分支,如果callbackRunnable)不是null,则直接执行callback.run()方法,如果callbacknull,则将msg作为参数传给handleMessage()方法去处理,这样就是我们常见的处理方法了。

Message.target与Handler

特别需要注意Message中的target成员变量,它是指向自己的发送者,这一点意味着什么呢?

意味着:一个有Looper的线程可以有很多个Handler,这些Handler都是不同的对象,但是它们都可以将Message对象发送到同一个MQ中,Looper不断的从MQ中获取这些消息,并将消息交给它们的发送者去处理。一个MQ是可以对应多个Handler的(多个Handler都可以往同一个MQ中消息入队)。

下图可以简要的概括下它们之间的关系。


Looper,MessageQueue,Handler,Message

总结

  • Looper调用prepare()进行初始化,创建了一个与当前线程对应的Looper对象(通过ThreadLocal实现),并且初始化了一个与当前Looper对应的MessageQueue对象。
  • Looper调用静态方法loop()开始消息循环,通过MessageQueue.next()方法获取Message对象。
  • 当获取到一个Message对象时,让Message的发送者(target)去处理它。
  • Message对象包括数据,发送者(Handler),可执行代码段(Runnable)三个部分组成。
  • Handler可以在一个已经Looper.prepare()的线程中初始化,如果线程没有初始化Looper,创建Handler对象会失败。
  • 一个线程的执行流中可以构造多个Handler对象,它们都往同一个MQ中发消息,消息也只会分发给对应的Handler处理。
  • Handler将消息发送到MQ中,Messagetarget域会引用自己的发送者,Looper从MQ中取出来后,再交给发送这个MessageHandler去处理。
  • Message可以直接添加一个Runnable对象,当这条消息被处理的时候,直接执行Runnable.run()方法。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容