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()
来进行初始化,一个线程只能创建一个与之对应的Looper
,Looper
初始化的时候会创建一个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);
}
注意到它对Message
的target
属性进行了赋值,这样这条消息就知道自己是被谁发送的了。然后将消息加入到队列中。
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
方法有两个分支,如果callback
(Runnable
)不是null
,则直接执行callback.run()
方法,如果callback
是null
,则将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中,Message
的target
域会引用自己的发送者,Looper
从MQ中取出来后,再交给发送这个Message
的Handler
去处理。Message
可以直接添加一个Runnable
对象,当这条消息被处理的时候,直接执行Runnable.run()
方法。
Android中线程间通信原理分析:Looper,MessageQueue,Handler的更多相关文章
- Android线程间通信机制——深入理解 Looper、Handler、Message
在Android中,经常使用Handler来实现线程间通信,必然要理解Looper , Handler , Message和MessageQueue的使用和原理,下面说一下Looper , Handl ...
- 源码分析Android Handler是如何实现线程间通信的
源码分析Android Handler是如何实现线程间通信的 Handler作为Android消息通信的基础,它的使用是每一个开发者都必须掌握的.开发者从一开始就被告知必须在主线程中进行UI操作.但H ...
- Android中线程通信的方式
Android 跨线程通信 android 中是不允许在主线程中进行 网络访问等事情的因为UI如果停止响应5秒左右的话整个应用就会崩溃,到Android4.0 以后 Google强制规定,与网络相关的 ...
- Android笔记(三十一)Android中线程之间的通信(三)子线程给主线程发送消息
先看简单示例:点击按钮,2s之后,TextView改变内容. package cn.lixyz.handlertest; import android.app.Activity; import and ...
- 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析
不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08 ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...
- Windows环境下多线程编程原理与应用读书笔记(4)————线程间通信概述
<一>线程间通信方法 全局变量方式:进程中的线程共享全局变量,可以通过全局变量进行线程间通信. 参数传递法:主线程创建子线程并让子线程为其服务,因此主线程和其他线程可以通过参数传递进行通信 ...
- 线程间通信(等待,唤醒)&Java中sleep()和wait()比较
1.什么是线程间通信? 多个线程在处理同一资源,但是任务却不同. 生活中栗子:有一堆煤,有2辆车往里面送煤,有2辆车往外拉煤,这个煤就是同一资源,送煤和拉煤就是任务不同. 2.等待/唤醒机制. 涉及的 ...
- Java 中如何实现线程间通信
世界以痛吻我,要我报之以歌 -- 泰戈尔<飞鸟集> 虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信. 关于线程间通信本文涉及到 ...
- Object类中wait带参方法和notifyAll方法和线程间通信
notifyAll方法: 进入到Timed_Waiting(计时等待)状态有两种方式: 1.sleep(long m)方法,在毫秒值结束之后,线程睡醒,进入到Runnable或BLocked状态 2. ...
随机推荐
- 【LG3722】[HNOI2017]影魔
[LG3722][HNOI2017]影魔 题面 洛谷 题解 先使用单调栈求出\(i\)左边第一个比\(i\)大的位置\(lp_i\),和右边第一个比\(i\)大的位置\(rp_i\). 考虑\(i\) ...
- #6435. 「PKUSC2018」星际穿越
考场上写出了70分,现在填个坑 比较好写的70分是这样的:(我考场上写的贼复杂) 设\(L(i)=\min_{j=i}^nl(j)\) 那么从i开始向左走第一步能到达的就是\([l(i),i-1]\) ...
- eclipse - 新建jsp页面默认模板设置
有时候我们自己如果没有现成的JSP模板时,系统一般会自动生成如下页面: 这个页面显然并不是我们所需要的,所以我们需要修改默认模板 进入 修改 <%@ page language="ja ...
- Spring restTemplate
什么是RestTemplate RestTemplate是Spring提供的用于访问Rest服务的客户端,提供了多种便捷访问远程HTTP服务的方法,能够大大提高客户端的编写效率. 项目中注入Res ...
- jenkins maven设置settings.xml
环境:jenkins.2.89.3 1.安装settings.xml管理插件Config File Provider Plugin 系统管理->管理插件->搜索Config File P ...
- js.ajax优缺点,工作流程
1.ajax的优点 Ajax的给我们带来的好处大家基本上都深有体会,在这里我只简单的讲几点: 1.最大的一点是页面无刷新,在页面内与服务器通信,给用户的体验非常好. 2.使用异步方式与服务器通信,不 ...
- 解决了一个困扰我近一年的vim显示中文乱码的问题
今天解决了vi命令打开日志文件中文总是显示乱码的问题.由于项目组中的日志包含一些特殊字符,所以使用vim打开日志文件时总是不能正确识别出文件字符编码.此时用:set fileencoding命令可以看 ...
- MySQL日志系统:redo log与binlog
日志系统主要有redo log(重做日志)和binlog(归档日志).redo log是InnoDB存储引擎层的日志,binlog是MySQL Server层记录的日志, 两者都是记录了某些操作的日志 ...
- Python自动化运维
一.DNS域名轮询业务监控 链接:https://www.cnblogs.com/baishuchao/articles/9128953.html 二.文件内容差异对比方法 链接:https://ww ...
- linux shell 压缩解压命令
.tar 解包:tar xvf FileName.tar打包:tar cvf FileName.tar DirName(注:tar是打包,不是压缩!)———————————————.gz解压1:gun ...