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

书史足自悦,安用勤与劬。这篇文章主要讲述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中的线程间通信方案 Looperandroid.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()方法来获取消息
MessageQueueandroid.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去处理。
Messageandroid.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,可以理解为一个可以被执行的代码片段。
Handlerandroid.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); }

【Android中线程间通信原理分析(Looper,MessageQueue,Handler)】它又调用了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中消息入队)。
下图可以简要的概括下它们之间的关系。
Android中线程间通信原理分析(Looper,MessageQueue,Handler)

文章图片

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()方法。
最后的话:FOR Freedom 、最后,安利一些去油+管的稳定好用的+速器代理。祝大家在YouTube上想消磨时间的逛个痛快,想锻炼英语获取知识的学有所成:
加速器推荐 免费方案 付费方案 官方网站
一枝红杏加速器 免费方案暂无,稳定高速 输入8折优惠码wh80,年付只需80元/年 官网直达
安云加速器 最好用的外贸VPN 最低¥30/月 官网直达
LoCo加速器 每天免费2小时 最低¥15/月 官网直达
【稳定好用的+速器代理:  http://whosmall.com/go/yzhx】
本文标签:  YouTube  看youtube视频  学习外语上youtube  YouTube1080p  上YouTube
转自  SUN‘S BLOG - 专注互联网知识,分享互联网精神!
原文地址  《你知道YouTube到底有多强大吗?如何上YouTube及最强攻略》
相关阅读《Chrome 扩展 Stylish :给不喜欢某个网站一键「换肤」》
相关阅读《将 QQ 音乐、网易云音乐和虾米音乐资源「整合」一起的Chrome 扩展Listen 1》
相关阅读《8 个「新标签页」Chrome 扩展: 教你把 New Tab 页面玩的溜溜溜》
相关阅读《7 款实用 Chrome 扩展推荐:帮你提升 Chrome 使用体验》
相关阅读《无扩展就不是 Chrome 了:15 款优质的Chrome 扩展推荐给大家》
相关阅读《12 款不能少的使网页浏览获得的最佳体验Chrome 扩展》
相关阅读《5 款可以带来幸福感的 Chrome 扩展》
相关阅读:  《关于 Android 中的 Palette 类的使用案例:色彩自适应的 Toolbar》
相关阅读:《GIT能做什么、它和SVN在深层次上究竟有什么不同》
相关阅读:《分享一些对开发者最有用的、用户友好和功能丰富的Google Chrome扩展工具》
相关阅读:《分享一些实际Android开发过程中很多相见恨晚的工具或网站》
相关阅读:《我是 G 粉,一直关注 Google,最近 Google 有一些小动作,可能很多人不太了解》
相关阅读:《机器学习引领认知领域的技术创新,那么SaaS行业会被机器学习如何改变?》
相关阅读:《VPS 教程系列:Dnsmasq + DNSCrypt + SNI Proxy 顺畅访问 Google 配置教程》
相关阅读:  最有用:2017最新能上Google、Facebook的方法
相关BLOG:SUN’S BLOG  - 专注互联网知识,分享互联网精神!去看看:www.whosmall.com






    推荐阅读