自问自答的两个问题

在我们去讨论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调用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()方法。

Android中线程间通信原理分析:Looper,MessageQueue,Handler的更多相关文章

  1. Android线程间通信机制——深入理解 Looper、Handler、Message

    在Android中,经常使用Handler来实现线程间通信,必然要理解Looper , Handler , Message和MessageQueue的使用和原理,下面说一下Looper , Handl ...

  2. 源码分析Android Handler是如何实现线程间通信的

    源码分析Android Handler是如何实现线程间通信的 Handler作为Android消息通信的基础,它的使用是每一个开发者都必须掌握的.开发者从一开始就被告知必须在主线程中进行UI操作.但H ...

  3. Android中线程通信的方式

    Android 跨线程通信 android 中是不允许在主线程中进行 网络访问等事情的因为UI如果停止响应5秒左右的话整个应用就会崩溃,到Android4.0 以后 Google强制规定,与网络相关的 ...

  4. Android笔记(三十一)Android中线程之间的通信(三)子线程给主线程发送消息

    先看简单示例:点击按钮,2s之后,TextView改变内容. package cn.lixyz.handlertest; import android.app.Activity; import and ...

  5. 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析

    不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08       ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...

  6. Windows环境下多线程编程原理与应用读书笔记(4)————线程间通信概述

    <一>线程间通信方法 全局变量方式:进程中的线程共享全局变量,可以通过全局变量进行线程间通信. 参数传递法:主线程创建子线程并让子线程为其服务,因此主线程和其他线程可以通过参数传递进行通信 ...

  7. 线程间通信(等待,唤醒)&Java中sleep()和wait()比较

    1.什么是线程间通信? 多个线程在处理同一资源,但是任务却不同. 生活中栗子:有一堆煤,有2辆车往里面送煤,有2辆车往外拉煤,这个煤就是同一资源,送煤和拉煤就是任务不同. 2.等待/唤醒机制. 涉及的 ...

  8. Java 中如何实现线程间通信

    世界以痛吻我,要我报之以歌 -- 泰戈尔<飞鸟集> 虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信. 关于线程间通信本文涉及到 ...

  9. Object类中wait带参方法和notifyAll方法和线程间通信

    notifyAll方法: 进入到Timed_Waiting(计时等待)状态有两种方式: 1.sleep(long m)方法,在毫秒值结束之后,线程睡醒,进入到Runnable或BLocked状态 2. ...

随机推荐

  1. 【LG3722】[HNOI2017]影魔

    [LG3722][HNOI2017]影魔 题面 洛谷 题解 先使用单调栈求出\(i\)左边第一个比\(i\)大的位置\(lp_i\),和右边第一个比\(i\)大的位置\(rp_i\). 考虑\(i\) ...

  2. #6435. 「PKUSC2018」星际穿越

    考场上写出了70分,现在填个坑 比较好写的70分是这样的:(我考场上写的贼复杂) 设\(L(i)=\min_{j=i}^nl(j)\) 那么从i开始向左走第一步能到达的就是\([l(i),i-1]\) ...

  3. eclipse - 新建jsp页面默认模板设置

    有时候我们自己如果没有现成的JSP模板时,系统一般会自动生成如下页面: 这个页面显然并不是我们所需要的,所以我们需要修改默认模板 进入 修改 <%@ page language="ja ...

  4. Spring restTemplate

    什么是RestTemplate RestTemplate是Spring提供的用于访问Rest服务的客户端,提供了多种便捷访问远程HTTP服务的方法,能够大大提高客户端的编写效率.   项目中注入Res ...

  5. jenkins maven设置settings.xml

    环境:jenkins.2.89.3 1.安装settings.xml管理插件Config File Provider Plugin  系统管理->管理插件->搜索Config File P ...

  6. js.ajax优缺点,工作流程

    1.ajax的优点 Ajax的给我们带来的好处大家基本上都深有体会,在这里我只简单的讲几点: 1.最大的一点是页面无刷新,在页面内与服务器通信,给用户的体验非常好.  2.使用异步方式与服务器通信,不 ...

  7. 解决了一个困扰我近一年的vim显示中文乱码的问题

    今天解决了vi命令打开日志文件中文总是显示乱码的问题.由于项目组中的日志包含一些特殊字符,所以使用vim打开日志文件时总是不能正确识别出文件字符编码.此时用:set fileencoding命令可以看 ...

  8. MySQL日志系统:redo log与binlog

    日志系统主要有redo log(重做日志)和binlog(归档日志).redo log是InnoDB存储引擎层的日志,binlog是MySQL Server层记录的日志, 两者都是记录了某些操作的日志 ...

  9. Python自动化运维

    一.DNS域名轮询业务监控 链接:https://www.cnblogs.com/baishuchao/articles/9128953.html 二.文件内容差异对比方法 链接:https://ww ...

  10. linux shell 压缩解压命令

    .tar 解包:tar xvf FileName.tar打包:tar cvf FileName.tar DirName(注:tar是打包,不是压缩!)———————————————.gz解压1:gun ...