“发送消息-处理消息” 是最基础的消息处理流程。在Android系统中,这一流程是通过Handler+Message进行的。下面介绍一下Handler的基本用法以及其深层的原理。
基本用法
创建一个handler,发送与处理消息。
下面我创建一个handler,同时定义了两个消息,并添加了消息处理逻辑:
private static final int MSG_SHOW_TOAST = 1;
private static final int MSG_FINISH = 2;
private Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case MSG_SHOW_TOAST:
Toast.makeText(MainActivity.this, "收到了消息1", Toast.LENGTH_SHORT).show();
break;
case MSG_FINISH:
finish();
break;
}
}
};
此时就可以向这个handler发送消息了:
handler.sendEmptyMessage(MSG_SHOW_TOAST);
发送消息后,会弹出提示“收到了消息1”。
还可以发送延时消息:
handler.sendEmptyMessageDelayed(MSG_FINISH, 2000);
发送消息后,等待2秒钟,当前Activity退出(之前我们添加的消息处理代码中,收到MSG_FINISH消息时执行了finish()方法)。
基本用法很简单,现在我们来探究一下handleMessage方法收到的msg对象与我们发送的消息有什么关系。
考察sendEmptyMessage的源码如下:
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);
}
在这里看到,sendEmptyMessage方法内部调用的也是sendEmptyMessageDelayed方法,也就是说普通消息是延时为0的延时消息。
Handler的多个方法都是这样设计的,我们在开发过程中也要有意识的这样设计代码,可以提高代码的复用性、减少书写相似的逻辑。
sendEmptyMessageDelayed方法内通过Message.obtain()方法拿到了一个Message对象,并将该对象的what数据修改为我们传递的参数。这里的Message对象就是上面handleMessage方法收到的消息对象。通过判断该对象中的what的值,就可以得到具体的消息。
关于Message.obtain()方法,后面会做更详细的介绍。现在只需要知道该方法获取一个空消息对象。
更多的消息类型
上面介绍了如何使用Handler发送简单的消息以及消息是如何接收处理的。接下来看一下Handler还支持什么形式的消息。
带参数的消息
考察Message类,可以看到如下的变量定义:
public class Message {
public int what;
public int arg1;
public int arg2;
public Object obj;
}
与之相对的,Handler中定义了如下的方法:
public class Handler {
public final Message obtainMessage();
public final Message obtainMessage(int what);
public final Message obtainMessage(int what, int arg1, int arg2);
public final Message obtainMessage(int what,Object obj);
public final Message obtainMessage(int what, int arg1, int arg2, Object obj);
public final boolean sendMessage(Message msg);
public final boolean sendMessageDelayed(Message msg, long delayMillis);
}
这里表明,一个Message对象可以包含两个int数据以及一个Object数据。Message对象可以通过sendMessage发送或通过sendMessageDelayed延时发送。在使用时可以这样做:
private void sendMessage(){
Message msg = handler.obtainMessage(MSG_SHOW_TOAST, 5, 6, "我有%d个苹果,%d个梨。");
handler.sendMessageDelayed(msg, 2000);
}
private static final int MSG_SHOW_TOAST = 1;
private Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case MSG_SHOW_TOAST:
String str = String.format((String) msg.obj, msg.arg1, msg.arg2);
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
break;
}
}
};
// 执行结果:2秒后展示Toast,内容为“我有5个苹果,6个梨。”
另外,也可以通过obtainMessage()方法获取一个空的Message对象,然后自由定制其消息内容。
Runnable消息
除了Message消息类型,Handler还支持发送Runnable对象作为消息。比如:
handler.post(new Runnable() {
@Override
public void run() {
Log.i("YQ","来自Runnable的消息");
}
});
来看一下这个方法内部是如何实现的:
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
post内调用了getPostMessage方法,将Runnable对象封装成了一个Message对象,后续的流程就和Message的发送一致了。在getPostMessage方法内,将Runnable对象赋值给了Message内的callback变量。这个过程中用到的callback变量是私有的、getPostMessage方法也是私有的,也就是说上层调用者不能自己构造一个带Runnable的Message对象并发送。在另一方面,post所发送的消息在处理的时候是直接调用参数中的Runnable实例的,这里并没有走到Handler类中的handleMessage方法。
我们来通过源码了解一下其执行过程:
public class Handler {
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
// 省略了部分代码...
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
}
在消息分发方法dispatchMessage中,代码先判断Message对象中是否包含callback对象,如果有,则直接执行其run方法,如果没有则调用handleMessage方法。
也就是说,带Runnable的消息优先级是高于带what的消息的。为了避免构建了带what与Runnable的消息导致消息传递错误,系统屏蔽了callback变量的访问权限,仅允许通过post方法发送Runnable消息。
在另一方面,dispatchMessage也是有漏洞的,因为子类可以覆盖该方法并修改消息派发策略。在开发中一定要规避该问题。为了避免手滑导致继承方法错误,可以使用Handler中带Callback接口的构造方法,Handler(Callback callback)。
dispatchMessage方法作为Handler中关键的消息分发方法,其实应该声明为final的以禁止子类对其进行覆盖。
指定时间的消息
上面看到了及时消息与延时消息。Handler还提供了指定时间的消息,这种类型的方法可以指定消息处理的具体时间:
boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
boolean sendMessageAtTime(Message msg, long uptimeMillis)
boolean postAtTime(Runnable r, long uptimeMillis)
而实际上,及时消息和延时消息,最终都是调用的定时消息方法:
// 前面提到了sendMessage内调用的是sendMessageDelayed,并将delayMillis设为0。
public final boolean sendMessageDelayed(@android.annotation.NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
// 这里将延时时间加上当前时间得到实际的消息处理时间。
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
注意到这里使用的时间时系统启动时间SystemClock.uptimeMillis(),而不是UTC时间。因为用户可以修改UTC时间,从而导致消息时间不准确。
除此之外,Handler还提供了发送高优先级消息的方法:
boolean postAtFrontOfQueue(Runnable r)
boolean sendMessageAtFrontOfQueue(Message msg)
但是强烈建议不要使用这两个方法。按照Api上的注释,只有非常特殊的情况下才能使用,因为这样做可能会引起很多问题。
消息的判断与移除
使用下面的方法可以检查当前消息队列中是否包含指定消息,以及删除消息队列中的指定消息。
boolean hasMessages(int what)
boolean hasCallbacks(Runnable r)
void removeMessages(int what)
void removeCallbacks(Runnable r)
消息池
之前介绍了,在发送自定消息时,需要使用Handler.obtainMessage系列方法获取一个消息实例。为什么不能直接创建呢?我们先来看一下这个方法内是什么:
public final Message obtainMessage() {
return Message.obtain(this);
}
再观察Message.obtain内的代码:
public static Message obtain(Handler h) {
Message m = obtain();
m.target = h;
return m;
}
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
重点是obtain()方法内的内容,可以看到这里有一个链表形式的消息池(sPool),代码优先从池中获取消息,如果池是空的,则新建消息。
所以很容易理解,使用obtain方法获取消息对象,是为了避免重复创建消息对象,从而避免因频繁的创建销毁Message对象造成的内存抖动现象。
最后,我们来看一下Message对象是如何回收的:
// 定义消息池最大容量
private static final int MAX_POOL_SIZE = 50;
// 这是Message类内的方法
void recycleUnchecked() {
// 第一步 清空消息对象内信息,内容很简单,已省略 。。。
// 将当前消息放入池中
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
handler与线程
Looper参数
上一节我们使用Handler的默认构造方法创建了一个Handler。考察其构造过程,我们可以发现方法内对包含一句对looper变量的赋值操作(为了说明方便,这里省略的很多代码):
public Handler() {
// 。。。
mLooper = Looper.myLooper();
// 。。。
}
观察其他的构造方法,可以看到,还有两个构造方法可以接收Looper参数。
public Handler(Looper looper);
public Handler(Looper looper, Handler.Callback callback);
看到这里可能会一脸发懵。Looper是哪里来的?为什么要出现在这里?出现在这里有什么用处?和Handler有什么关系?一连串的问题一一冒了出来。
先从Looper怎么使用入手,了解一下其用法,然后在深究其原理。
创建一个Looper
首先,主线程(又叫UI线程,是Android应用启动时的首个线程,也是代码运行所在的默认线程)中已经包含了一个Looper实例。主线程的Looper实例是使用Looper.prepareMainLooper静态方法创建的,获取主线程的Looper可以使用Looper.getMainLooper()静态方法。
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
使用Looper.prepare()方法可以在线程中创建一个而且也只能创建一个Looper实例。来看一下方法内容:
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));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
这里注意以下几点:
- 存在 prepare(boolean) ,其中的boolean参数只有在主线程中创建的时候会使用false作为参数。
- 新创建的Looper实例放在了ThreadLocal中作为线程局部存储数据进行保存。这保证了数据只能被当前线程读写。
- 使用Looper.myLooper可以获取到当前线程的Looper。注意在当前线程未调用prepare前,该方法会返回空。
- 主线程Looper实例被保存在sMainLooper静态变量中,在任意位置可以通过getMainLooper静态方法获取。
更多ThreadLocal相关的的内容请移步百度。
Looper作为Handler参数有何用途
上面提到一个线程可以有一个也最多只能由一个Looper实例。那Handler需要这个Looper实例做什么用呢?
首先通过Handler的构造方法,可以发现Handler里面对于Looper是强要求的,要么由外部传入,要么使用Looper.myLooper()获取当前线程的Looper。如果获取不到,会直接抛出异常:
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
异常描述中提示:当前线程没有调用Looper.prepare()方法。
查看代码,发现代码中获取了Looper实例的mQueue变量:
mQueue = mLooper.mQueue;
继续看mQueue的使用场景,看到它参与了消息发送的主要逻辑:
public boolean sendMessageAtTime(@NonNull 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(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
这里mQueue的类型是MessageQueue,翻译过来就是“消息队列”。Handler在发送消息的时候,实际就是发送到了这个消息队列中了。在另一端MessageQueue收到消息之后,实际上是发送到了Looper所在的线程中了。而相关的消息处理逻辑就是在对应的线程中执行的。
在之前的例子中,我们在主线程中使用默认构造方法创建了一个Handler实例,该Handler使用了当前线程的Looper对象(也就是MainLooper)。之后使用该Handler实例发送消息时,发送消息时所处的线程无论是主线程还是子线程,消息执行的线程(handleMessage方法执行的线程或Runnable对象run方法执行的线程)都只能是主线程。
下面介绍一个在子线程中使用Handler的方法。
使用HandlerThread
Android系统SDK提供了HandlerThread类,将线程、Looper、Handler结合在了一起,方便开发者进行使用。该类可以新建一个子线程,并通过Handler发送消息的方式,在子线程中处理相关消息逻辑。
下面是一个简单的使用示例
// 创建一个HandlerThread对象,参数为线程的名称
// 要养成为线程合理命名的好习惯,对于后续的开发、调试、查找问题等都有很大的帮助。
HandlerThread handlerThread = new HandlerThread("TestHandlerThread");
// 和普通线程一样,需要使用start方法启动线程。
handlerThread.start();
// 使用handlerThread内的Looper实例创建Handler对象
Handler handler = new Handler(handlerThread.getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
// 这里的代码是在handlerThread线程内执行的
return false;
}
});
handler.post(new Runnable() {
@Override
public void run() {
// 这里的代码是在handlerThread线程内执行的
}
});
// 使用结束后退出线程
handlerThread.quit();
HandlerThread.getLooper()方法需要等待线程启动后创建Looper实例,所以在实际使用时应该先启动线程,并在之后择机获取Looper创建Handler。
Looper介绍
Looper就是循环,在这里特指消息循环。上节提到,一个Looper对应一个线程中,用于向该线程发送消息。这是怎么做到的呢?下面简单介绍一下。
消息队列
看下图,任何线程都可以通过Handler发送消息。而Handler最终将消息发送到了消息队列MessageQueue中。Looper从消息队列中等待、获取消息,并将消息发往线程进行处理。
消息处理循环Looper一直在循环查询消息,为什么没有出现cpu耗尽的情况?原因就在于这里的消息队列MessageQueue是个阻塞队列,而这里的情形就是多线程同步问题中典型的“生产者-消费者”问题。
与消息相关的坑
Handler的内存泄漏
在Activity内创建并使用Handler容易引起内存泄漏。内存泄漏的原因可能是因为handler发送了一个延时消息,可能是handler在等待其他线程的处理结果。原因会多种多样,最终因为作为内部类的handler对象持有了Activity的引用,导致Activity内存泄漏。
解决方法很简单,将Handler定义为静态内部类,并使用弱引用持有Activity实例即可。直接上代码:
private static class MyHandler extends Handler {
private Reference<MainActivity> activityReference;
public MyHandler(MainActivity activity) {
this.activityReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(@NonNull Message msg) {
MainActivity activity = activityReference.get();
if (activity != null && !activity.isFinishing()) {
// Activity没有被回收也没有在销毁中,可以做事情了。。。
}
}
}
弱引用对象如果没有其他的强引用位置,将会被GC回收。
代码中创建内部类形式的Handler时,IDE会直接提示内存泄漏风险。关注IDE提供的代码warning,并按提示进行修改,可以快速提高代码质量,而且能够解决潜在的bug。
View中的post
Android提供的View类中包含了post方法,用于向主线程发送及时或延时的Runnable消息。该方法的行为与Handler中的post方法看起来是一致的,而且实际上View的方法内部也确实调用的Handler的中的post方法。但实际上,通过View发送的消息,只有当View在屏幕上显示的时候才会执行。如果View没有在屏幕上显示,那这个消息可能就永远不会被执行了。
避免通过handler在多个类之间传递消息
Handler出现的目的就是传递消息。但是作为一种消息传递机制,他的长处在于“在多个线程之间传递消息”,而不是“在类之间传递消息”。
考虑到Message消息类型的单一性以及内容的任意性,当用他作为多个类之间的消息接口时,会导致代码无法维护。而类之间传递消息应该使用接口(interface)定义,这样才能保证代码的单一性、局部性和可维护性。
作为开发者,切勿犯懒,更不能认为少定义了一个接口代码就会显得更简洁。
专业的事情交给专业的工具去做,让手里的工具箱丰富起来,避免手里拿着锤子的时候看什么都是钉子。
相关知识点
- 线程
- 线程局部存储(ThreadLocal)
- 多线程数据同步
- “生产者-消费者”问题
- 弱引用/软引用
网友评论