结合源码说说使用Handler发送异步消息的实现过程
在Android开发中,我们经常会遇到这样一种情况:如果在UI界面上执行一段很耗时的代码,比如我们在界面上点击了一个“下载”按钮,那么我们需要执行网络请求,此时如果下载量大,或网络比较差,很容易就会造成ANR(Application Not Responding)错误,为了保证不影响UI线程,我们通常会创建一个新的线程去执行我们的耗时的代码。当我们的耗时操作完成后,再用handler来通知UI线程刷新页面。
对于刚接触Handler的人来讲,可能会觉得这个东西真方便,一下子就解决了跨线程的通信,或许还会觉得有点神秘,它是怎么做到的呢?之所以觉得神秘我想应该是API帮我们做了太多事,如果不去看源码的话,它就像个黑匣子,我们在外面丢点东西进去,呆会它就从另一头出来了。面对一个我们不熟悉的黑匣子,当它带给我们惊喜的时候我们通常会觉得它很神秘,当它给我们带来麻烦的时候我们通常会觉得它很可怕。要想解开心里的疑惑当然只有揭开它的面纱看看它的内部是如何运作的了。
原理
先化繁为简,假设有两个线程,A线程要传个字符串给B线程该怎么实现?
我想最简单的就是设一个全局变量,然后在A线程中为全局变量赋值,而在B线程中去取值就完了吧,当然赋值与取值要做好同步工作,那么从本质上说Handler的消息处理机制也是一样的,请看下面的示意图:
现在我们假设图中的线程2是这样一个线程:这个线程几乎啥也不干,它一启动之后就创建一个叫做Looper的对象,并且这个对象与创建它的线程是一一对应的。好,做完这个事,这个线程就不再做别的事了,按照一般的情况,一个线程执行完run()方法里的代码后这个线程就结束了,但我们这里这个线程不会结束,不是因为这个线程比较特殊,而是它创建的这个Looper对象有点特殊。这个Looper对象有一个方法是可以阻塞的,这个方法是干吗的呢,是用来从消息列表中取消息用的,当消息列表中有消息时它会调用Looper对象的成员“mQueue”把消息取出来,没有消息时它就在那里等着,这样一来这个方法就永远执行不完,那么线程2也就不会结束了。由于线程1和线程2操作的是同一个变量,所以就实现了线程间的通信了。
案例设计
好,为了搞清楚Handler是怎样具备跨线程通信这个能力的,我们设计一个这样的例子:跟我们通常使用的方式反过来,即在主线程中发送消息,然后在子线程中处理消息。之所以这样设计是因为主线程的源码比较复杂,不容易挑出与Handler有必然联系的代码来分析。而在子线程中就容易多了,我们自己定义一个子线程,只写与Handler相关的代码,其它的什么都不做,这样更容易看清Handler的本质。
代码分析
下面先给个简单的流程图,然后再结合例子和源码来讲讲Handler是怎么实现的。
上面图中红框中的几个步骤是在源码中执行的,对于程序员来说相当于不可见的。剩下的一头一尾则是我们经常要写到的代码,是我们非常熟悉的。
一、例子
这里先把例子写出来,跑起来看一下结果,然后再来分析是如何实现的。按照上面设计的案例,从UI线程中发送消息,然后在子线程中处理消息。
1.1、 自定义一个SubThread类,继承Thread类
这个线程是用来取消息及处理消息的。代码如下:
/**
* 自定义子线程
*
* @author Huangming 2017/10/25
*/
public class SubThread extends Thread {
private Looper mLooper;
public SubThread() {
super("SubThread");
}
@Override
public void run() {
Looper.prepare();
mLooper = Looper.myLooper();
Looper.loop();
}
public Looper getLooper() {
return mLooper;
}
}
非常简单,在run()
方法中初始化Looper,并执行 Looper.loop()
方法,然后暴露一个Looper对象出来供其它线程创建Handler的时候使用,这样就完了。上面的loop()
方法就是可阻塞的,用来取消息,没消息的时候就等待消息。具体的呆会再说。
1.2、 再看UI线程中发消息的代码
private void initView() {
System.out.println("UI线程id = " + Thread.currentThread().getId() + " name = " + Thread
.currentThread().getName());
final SubThread subThread = new SubThread();
subThread.start();
findViewById(R.id.btnSendMsg).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 注意新建Handler的时候传了子线程的looper变量进去
Handler handler = new Handler(subThread.getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
System.out.println("msg.what = " + msg.what);
System.out.println("处理消息的线程id = " + Thread.currentThread().getId()
+ " name = " + Thread.currentThread().getName());
return false;
}
});
System.out.println("发消息的线程:" + Thread.currentThread().getId());
// 传一个空消息,msg.what等于2
handler.sendEmptyMessage(2);
}
});
}
这个方法是在MainActivity中的初始化view方法,里面有一个按钮,点击按钮就发一条消息去子线程。代码也是非常的简单,就是将收发消息的线程打印出来,没有任何干扰。
值得注意的是:
新建Handler的时候要将子线程的looper变量作为参数传进去,这决定了这个handler发送的消息是在哪个线程中处理。如果没有传looper参数,将使用当前线程的所对应的looper对象,如果当前线程没有调用过
Looper.prepare()
方法的话则会报错
跑起来,看一下结果:
可以看到,成功地将消息传递到了子线程中,在子线程中执行了handleMessage(Message msg)
方法内的代码。看到这里会不会觉得好简单?迫不及待地要想知道是怎么实现的了吧?接下来就分析一下源码:
二、源码分析
2.1、子线程中的源码分析
再来看一下子线程中run()
方法内的代码:
@Override
public void run() {
Looper.prepare();
mLooper = Looper.myLooper();
Looper.loop();
}
一共只有三行代码,其大致的功能如下:
第一行,初始化一个Looper对象,并将这个对象与子线程绑定;
第二行,将初始化好的Looper对象暴露出来;
第三行,循环去取消息,没消息的话就等待,一有消息又取出来
先看一下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));
}
这里面有一个ThreadLocal
对象sThreadLocal
,它是用来保证每个线程只能创建一个Looper对象的,所以在同一个线程中只能调一次Looper.prepare()
方法,否则会报错。当第一次调这个方法的时候就会创建一个Looper对象并与该线程绑定,所谓绑定就是一一对应,一个线程对应一个Looper对象,跟Map的原理一样,实现上也是用map来实现的,这个工作是由sThreadLocal
来完成的,sThreadLocal.set(new Looper(quitAllowed))
之后就将线程与新建的Looper对象一一对应起来了。具体如何实现的可以看看ThreadLocal类的set和get方法的源码或看看其它资料,这里就不展开了。
接下来看Looper.myLooper()
的源码:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
这就是将刚才set进去的Looper对象再取出来。
最后看下Looper.loop()
的源码:
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the 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;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (slowDispatchThresholdMs > 0) {
final long time = end - start;
if (time > slowDispatchThresholdMs) {
Slog.w(TAG, "Dispatch took " + time + "ms on "
+ Thread.currentThread().getName() + ", h=" +
msg.target + " cb=" + msg.callback + " msg=" + msg.what);
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
这个方法的代码有点多,容易扰乱视线,我们将它简化一下,把一些打印日志、判空等代码去掉,方便理解,简化后的核心代码如下:
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
msg.target.dispatchMessage(msg);
msg.recycleUnchecked();
}
}
精简后的代码就非常直观了,首先将与执行loop()
方法的线程绑定的Looper对象取出来,然后再将Looper对象中的消息队列取出来,等等,这个MessageQueue是从哪里来的?是在新建Looper的时候赋值的,看看Looper的构造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
所以这是在子线程中执行 Looper.prepare()
时候就赋值了。
好,将消息列表取出来后接下来就是一个死循环,不断地做以下操作:
调用MessageQueue的next()方法取出一个消息(注意这个方法是可能阻塞的,就是说消息列表内没有消息时会一直等着,直到有消息了把它取出来这行代码才算执行完)
调用msg.target的dispatchMessage(msg)方法来分发消息。msg.target其实就是一个Handler对象,等下会讲到。
回收这条消息
到这里还没看到在哪里调 handleMessage(Message msg)
方法,很显然就是在Handler类的 dispatchMessage(Message msg)
方法里了。跟下去看看:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
这里有三种方式将消息分发到不同的回调方法去处理:
分发给msg的callback变量来处理消息,callback是一个Runnable对象,对应在组建消息时的代码是这样的:
Message msg = Message.obtain(handler, new Runnable() { @Override public void run() { // 处理消息 } }); handler.sendMessage(msg);
上面新建的Runnable对象就是msg的callback变量。
分发给handler的mCallback变量来处理消息,mCallback是一个实现了Handler.Callback接口的对象,这个接口定义了一个
handleMessage(Message msg)
方法,这个方法就是用来处理消息的。对应在组建消息时的代码是这样的:Handler handler = new Handler(looper, new Handler.Callback() { @Override public boolean handleMessage(Message msg) { // 处理消息 return true; } });
这种方式也是我们例子里面使用的方式。
直接分发给handler的handleMessage(Message msg)方法来处理,此种情况适应于自定义Handler子类的场景。这个方法在Handler源码中是个空方法,只有继承Handler类时重写该方法才有意义。自定义继承Handler类的代码通常像这个样子:
class MyHandler extends Handler { @Override public void handleMessage(Message msg) { // 处理消息 } }
此时通过MyHandler的对象发送消息时,如果没有设置以上1、2两种情况的回调方法的话就会调用MyHandler里的handleMessage(Message msg)方法了。
好,到这里取消息处理消息的源码就分析完了,下面看看发送消息时的源码分析。
2.2、主线程中的源码分析
在1.2节中已经介绍了sendMessage()之前的代码了,这里只说说sendMessage()的源码。以我们例子中的代码为例,sendEmptyMessage(2)
的源码是:
public final boolean sendEmptyMessage(int what)
{
return sendEmptyMessageDelayed(what, 0);
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
Message msg = Message.obtain();
msg.what = what;
return sendMessageDelayed(msg, delayMillis);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
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);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
从以上源码中可以看到,发送消息的最终结果是调用了MessageQueue的enqueueMessage(msg, uptimeMillis)方法,执行入队操作,这里看一下sendMessageAtTime()方法中的mQueue是从哪里来的,搜一下,看到是在Handler构造方法中赋值的,源码如下:
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
很显然,这个mQueue就是从这里
// 注意新建Handler的时候传了子线程的looper变量进去
Handler handler = new Handler(subThread.getLooper(), new Handler.Callback(...));
传进来的,与前面提到的取消息(出队)操作是同一个对象,也就是说消息的入队出队都是由同一个对象来处理的。入队是在主线程中执行的,而出队是在子线程中执行的。MessageQueue则管理了这两个线程的同步操作。
再注意一下enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)
方法,第一行
msg.target = this;
这里将发送消息的handler赋给msg的target变量,所以在分发消息的时候,Looper.loop()方法中的这一行代码
msg.target.dispatchMessage(msg);
msg.target取出来的就发送该消息的handler。
至此,从发消息到处理消息的整个过程基本上就介绍完了,除了MessageQueue的入队出队源码没有分析外,其它的基本都点到了,希望能帮助大家加深对Handler机制的理解。
注:没有分析消息入队、出队的源码是因为这两部分涉及到一些native代码,没有再深入去看它的实现逻辑,还没有把握能正确地分析它们,只了解它们的功能就是一个是将消息保存起来,一个将消息取出来,并且是可阻塞的。要了解这部分的原理请参考其它资料。
最后,上源码:
demo源码:https://github.com/MingHuang1024/HandlerTest
由于水平有限,如果文中存在错误之处,请大家批评指正,欢迎大家一起来分享、探讨!
博客:http://blog.csdn.net/MingHuang2017
GitHub:https://github.com/MingHuang1024
Email: MingHuang1024@foxmail.com
微信:724360018