标签:view final apt 下载 设计 传递 方案 while java
在我们去讨论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。
这样就是一个我随意脑补的,简单的跨线程通信的方案。
有如下几点是值得注意的:
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方法干了什么:
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
的构造方法。
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper
的构造方法中,很关键的一句,它new了一个MessageQueue
对象,并自己维持了这个MQ的引用。
此时prepare()
方法的工作就结束了,接下来需要调用静态方法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()
方法来获取消息android.os.MessageQueue from Grepcode
对于MQ的源码,简单的看一下,构造函数与next()
方法就好了。
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
MQ的构造方法简单的调用了nativeInit()来进行初始化,这是一个jni方法,也就是说,可能是在JNI层维持了它这个消息队列的对象。
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的代码,我们得到了这些信息:
next()
方法是个死循环,在不停的访问MQ,从中获取消息出来返回给Looper
去处理。android.os.Message from Grepcode
Message
对象是MQ中队列的element,也是Handler
发送,接收处理的一个对象。对于它,我们需要了解它的几个成员属性即可。
Message
的成员变量可以分为三个部分:
what(int)
, arg1(int)
, arg2(int)
, obj(Object)
, data(Bundle)
等,一般用这些来传递数据。target
,它的类型是Handler
的,这个成员变量很重要,它标记了这个Message
对象本身是谁发送的,最终也会交给谁去处理。callback
,它的类型是Runnable
,可以理解为一个可以被执行的代码片段。android.os.Handler from Grepcode
Handler
对象是在API层面供给开发者使用最多的一个类,我们主要通过这个类来进行发送消息与处理消息。
通常我们调用没有参数的构造方法来进行初始化,使用起来大概是这样的:
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对象。
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
属性进行了赋值,这样这条消息就知道自己是被谁发送的了。然后将消息加入到队列中。
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
成员变量,它是指向自己的发送者,这一点意味着什么呢?
意味着:一个有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()
方法。祝大家在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 配置教程》
Android中线程间通信原理分析:Looper,MessageQueue,Handler
标签:view final apt 下载 设计 传递 方案 while java
原文地址:http://www.cnblogs.com/faceblog/p/6759929.html