第二节、Handler消息机制
一、为什么要设计handler
Java 多线程通信
Java 中有很多种方法实现线程之间相互通信访问数据,大概先简单的介绍两个典 型的,就不上代码了。 通过 synchronized 关键字以“上锁”机制实现线程间的通信。多个线程持 有同一个对象,他们可以访问同一个共享变量,利用 synchronized“上锁” 机制,哪个线程拿到了锁,它就可以对共享变量进行修改,从而实现了通 信。
使用 Object 类的 wait/notify 机制,执行代码 obj.wait();后这个对象 obj 所在的线程进入阻塞状态,直到其他线程调用了 obj.notify();方法 后线程才会被唤醒。
Android 多线程的特殊性
在上面的两个 Java 多线程通信的方法中都有一个共同的特点,那就是线程的阻 塞。利用 synchronized 机制拿不到锁的线程需要等拿到锁了才会继续执行操作, obj.wait();需要等 obj.notify();才会继续执行操作。 虽然 Android 系统是由 Java 封装的,但是由于 Android 系统的特殊性,Google 的 开发人员对 Android 线程的设计进行了改造。他们把启动 APP 时运行的主线程定 义为 UI 线程。 UI 线程负责所有你能想到的所有的跟界面相关的操作,例如分发绘制事件,分 发交互事件等可多了。由于其特殊性 Android 系统强制要求以下两点: 为保持用户界面流畅 UI 线程不能被阻塞,如果线程阻塞界面会卡死,若 干秒后 Android 系统抛出 ANR。 除 UI 线程外其他线程不可执行 UI 操作。 (此处只是简单介绍一下 UI 线程,后面会有专门一节分析 Android UI 线程。)
可 Java 中线程间通信又都是阻塞式方法,所以传统的 Java 多线程通信方式在 Android 中并不适用。 为此 Google 开发人员就不得不设计一套 UI 线程与 Worker 线程通信的方法。既 能实现多线程之间的通信,又能完美解决 UI 线程不能被阻塞的问题。
小结
说到了这里应该大概明白了当初设计 Handler 的初衷。 由于 Android 系统的特殊性创造了 UI 线程。 由于 UI 线程的特殊性创造了若干个 UI 线程与 Worker 线程通信的方法。 在这若干个线程通信方法中就包含了 Handler。 Handler 就是针对 Android 系统中与 UI 线程通信而专门设计的多线程通信机制。
Handler 提供的一些方法
一、发送消息类方法
1. sendEmptyMessage
boolean sendEmptyMessage (int what)
发送一个只有消息标识 waht 的空消息。该方法适用于不需要传递具体消息只是 单独的发通知时。
2.sendEmptyMessageAtTime
boolean sendEmptyMessageAtTime (int what, long uptimeMillis)
在具体指定的时间 uptimeMillis 发送一个只有消息标识 waht 的空消息。 uptimeMillis 为系统开机到当前的时间(毫秒)。
3. sendEmptyMessageDelayed
boolean sendEmptyMessageDelayed (int what, long delayMillis)
在过了 delayMillis 毫秒之后发送一个只有消息标识 waht 的空消息。
4. sendMessage
boolean sendMessage (Message msg) 发送一条消息。
5. sendMessageAtTime
boolean sendMessageAtTime (Message msg, long uptimeMillis) 在具体指定的时间 uptimeMillis 发送一条消息。uptimeMillis 为系统开机到当 前的时间(毫秒)。
6. sendMessageDelayed
boolean sendMessageDelayed (Message msg, long sendMessageDelayed ) 在过了 delayMillis 毫秒之后发送一条消息。
二、处理消息类方法
handleMessage
void handleMessage (Message msg) 负责接受消息,所有发送的消息都会返回该方法,注意!必须 Override 这个方法 才能接收消息。
三、切换线程类方法
1. post
boolean post (Runnable r) Runnable r 会运行在 handler 对象被创建的线程上。当我们在 UI 线程创建了 Hnadler 对象,在 Worker 线程调用 handler.post()方法时,Runnable 就会运行 在 UI 线程中。
2. postAtTime
boolean postAtTime (Runnable r, long uptimeMillis) 在具体指定的时间 uptimeMillis 让 Runnable 运行在 Handler对象被创建的线程 中。
3. postDelayed
boolean postDelayed(Runnable r, long delayMillis) 在具体指定的时间 delayMillis 之后让 Runnable 运行在 Handler 对象被创建的 线程中。
使用 Handler
在上节方法介绍中出现了 XXXAtTime(long uptimeMillis)和 XXXDelayed(long delayMillis)这两类控制时间的方法,两类方法的时间参数虽然都是毫秒,但是 代表的意义却不一样:
- XXXDelayed(long delayMillis) 中 的 时 间 参 数 是 指 从 当 前 时 间 开 始 delayMillis 毫秒后。
-
XXXAtTime(long uptimeMillis) 中 的 时 间 参 数 是 指 从 系 统 开 机 算 起 uptimeMillis 毫秒后。
利用静态方法 SystemClock.uptimeMillis()可以得到从系统开机到现在的毫秒 数,所以,下面两个语句执行的时间是相等的: - XXXDelayed(1000);
-XXXAtTime(SystemClock.uptimeMillis() + 1000)
二、Handler 实现原理 - 理论分析
image.png多线程通信
根据上面的结论可以知道 Handler 接收消息端是线程独立的,不管 handler 的引 用在哪个线程发送消息都会传回自己被实例化的那个线程中。 但显而易见的是 Handler 不可能是线程独立的,因为它的引用会在别的线程作为 消息的发送端,也就是说它本身就是多线程共享的引用,不可能独立存在于某个 线程内。 所以!Handler 需要一个独立存在于线程内部且私有使用的类帮助它接收消息! 这个类就是 Looper!
Looper - 线程独立
通过上节分析我们已经知道设计Looper就是为了辅助Handler接收消息且仅独立 于线程内部。那如何才能实现线程独立的呢? 好消息是 Java 早就考虑到了这一点,早在 JDK 1.2 的版本中就提供 ThreadLocal这么一个工具类来帮助开发者实现线程独立。
这里简单分析一下 ThreadLocal 的 使用方法,不分析实现原理了。Android 官网 - ThreadLocal API ThreadLocal 支持泛型,用于定义线程私有化变量的类型,实例化对象时可选 Override 一个初始化方法 initialValue(),这个方法的作用就是给你的引用变 量赋初始值,如果没有 Override 这个方法那么默认你的引用变量就是 null 的:
//定义一个线程私有的 String 类型变量
private static final ThreadLocal<String> local = new ThreadLocal<String>(){
// 设置引用变量的初始化值
@Override protected String initialValue() {
return super.initialValue();
} };
定义好了 ThreadLocal 之后还需要了解三个方法:
- get() 得到你的本地线程引用变量。
- set(T value)为你的本地线程引用变量赋值。
- remove() 删除本地线程引用变量。 是不是很简单呢?
有了 ThreadLocal 之后我们只需要把 Looper 存进去就能实现 线程独立了。
private static final ThreadLocal<Looper> mLooper = new ThreadLocal<Looper>();
到这里再梳理一下流程:
image.png
- Handler 引用可以多线程间共享。
- 当 Handler 对象在其他线程发送消息时,通过 Handler 的引用找到它所在线程的 Looper 接收消息。
- Looper 负责接收消息再分发给 Handler 的接收消息方法
Looper
但是!这样还会有一个问题,如果多个线程同时使用一个 Handler 发消息,Looper 该怎么办?给接收消息的方法上锁吗?显然不能这样做啊!于是就设计了 MessageQueue 来解决这个问题。
MessageQueue - 多线程同时发消息
为了防止多个线程同时发送消息 Looper 一下着忙不过来,于是设计一个 MessageQueue 类以队列的方式保存着待发送的消息,这样 Looper 就可以一个个 的有序的从 MessageQueue 中取出消息处理了。 既然 MessageQueue 是为 Looper 服务的,而 Looper 又是线程独立的,所以 MessageQueue 也是线程独立的。
小结
现在我们已经知道为了完成异步消息功能需要有 Handler 家族的四位成员共同合 作:
- Handler: 负责发送消息,为开发者提供发送消息与接收消息的方法。
- Message: 消息载体,负责保存消息具体的数据。
- MessageQueue:消息队列,以队列形式保存着所有待处理的消息。
Looper:消息接受端,负责不断从 MessageQueue 中取出消息分发给 Handler 接受消 息端。 这四位成员哪个都不是平白无故出现的。因为要规范化消息传递格式而定义了 Message;为了实现消息接收端只存在线程内部私有化使用而定义了 Looper;为 了解决多线程同时发送数据 Looper 分发消息处理时会产生的问题而设计 MessageQueue 队列化消息。
到这里你应该知道了 Handler 家族四位成员各自负责的是什么工作,以及他们自 身的特点特殊性,比如 Handler 是线程间共享的而 Looper 是线程独立的, MessageQueue 跟 Looper 又是一对一的。
Handler内存泄漏问题
由于重写了 handleMessage()方法相当于生成了一个匿名内部类:
class MyHandler extends Handler{
@Override public void handleMessage(Message msg) { } }
可是你有没有想过内部类凭什么能够调用外部类的属性和方法呢?答案就是内 部类隐式的持有着外部类的引用,编译器在创建内部类时把外部类的引用传入了 其中,只不过是你看不到而已。 既然 Handler 作为内部类持有着外部类(多数情况为 Activity)的引用,而 Handler 对应的一般都是耗时操作。当我们在子线程执行一项耗时操作时,用户退出程序, Activity 需要被销毁,而 Handler 还在持有 Activity 的引用导致无法回收,就会引 发内存泄漏。
解决方法分为两步
- 生成内部类时把内部类声明为静态的。
- 使用弱引用来持有外部类引用。 静态内部类不会持有外部类的引用,且弱引用不会阻止 JVM 回收对象。
静态内部类不会持有外部类的引用,且弱引用不会阻止 JVM 回收对象。
static class MyHandler extends Handler{ WeakReference<Activity> mActivity ;
public MyHandler(Activity activity){
mActivity = new WeakReference<Activity>(activity);
}
@Override public void handleMessage(Message msg) {
Activity activity = mActivity.get();
if (activity == null){
return; }// do something }
还有在Activity的onFinish()方法里调用handler.removeCallbacksAndMessages(null);
API level 1
Remove any pending posts of callbacks and sent messages whose objtoken . Iftoken is null, all callbacks and messages will be removed.。
意思就是当传入的参数为null时,则移除所有的messages,这样就有效的避免了由Handler引起的内存溢出。
第五节 Handler 机制实现原理总结
Message 缓存池
Android 的工程师们充分利用了 Java 的高级语言特性,即类中持有着一个类自身 的属性作为经典数据结构中的链表 next 指针,以静态属性属于类本身的特性实 现了链表的表头。这种模式给我了很大的启发,让我这种渣渣每逢想起都会惊讶 “还有这种操作?”。
为什么要有缓存池
了解完 Handler 整体机制后我猜测,Message 功能十分单一且状态很少,它只是 一个具体发送消息的载体,但是使用数量十分庞大,回收用过的 Message 不仅可 以有效的减少重复消耗系统资源且回收它的成本很低,所以何乐而不为呢?
谁负责回收 Message
我们使用 Message 时候知道调用 Message.obtain();方法可以从缓存池中取出一 个 Message,有存才能有取,我们什么时候回收它呢?从源码中发现,Looper 在分发 Message 给宿主 Handler 之后,确定了 Message 已经完成了它的使命直接 就会将它回收。所以我们完全不用担心这个,我们发送的每个消息最后都会被回 收。
真正的阻塞发生在 MessageQueue
MessageQueue 维持的消息队列也是靠跟 Message 缓存池同样的原理生成的,每 次消息出队时如果没有合适的待取出消息就会阻塞线程等待有合适的消息。 非常奇怪的是,MessageQueue 线程的方式不是传统使用 java 实现的,而是通过 JNI 调用 native 层的 C++代码实现的,C++代码中也实现了一套 Looper+MessageQueue+Handler,阻塞线程的方式是调用 Linux 的监听文件描述符 ePoll 实现的。 我的猜测是因为 Java 代码需要经过 JVM 的帮助才能跟系统接触,这一过程会消 耗性能,而 C++代码则直接可以绕过这一个环节。所以,使用 C++代码实现线程 阻塞可能是性能上的需求。
为什么推荐使用 Handler 实现线程间通信
Android 是事件型驱动的系统,刚创建一个应用程序的主线程里就会被 创建一个 Looper 来不断接受各种事件,所以说如果我们打开一个程序什么都不 操作,这个程序就有可能是阻塞状态的,因为他没有任何事件需要去处理。反之, 我们在自己的 UI 线程里执行一项耗时操作,主线程 Looper 一直在处理这个任务 而无法分身处理其它的事件这时候就有可能 ANR 了。 所以,不是 Handler 的技术多牛逼,是主线程用了 Handler 来通信,你是用别的 方法通信有可能会影响主线程 Looper的正常工作。
第六节 Handler 面试题全解析
做 Android 开发肯定离不开跟 Handler 打交道,它通常被我们用来做主线程与 子线程之间的通信工具,而 Handler 作为 Android 中消息机制的重要一员也确 实给我们的开发带来了极大的便利。 可以说只要有异步线程与主线程通信的地方就一定会有 Handler。 在面试中 Handler 也是经常被问的一个点,那么本篇文章就以问答的方式,带你 了解一下关于 Handler 的重要知识点。
1. 一个线程有几个 Looper?几个 Handler?
一个 Thread 只能有一个 Looper,一个 MessageQueen,可以有多个 Handler 以一个线程为基准,他们的数量级关系是:
Thread(1) : Looper(1) : MessageQueue(1) : Handler(N)
2.Handler 内存泄漏原因? 以及最佳解决方案?
泄露原因:
Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity, 那么该 Activity 会泄露。 这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。
解决方案
- 最直接的思路就是避免使用非静态内部类。使用 Handler 的时候,放在一个新建的 文件中来继承 Handler 或者使用静态的内部类来替代。静态内部类不会隐含的持有 外部类的引用,因此这个 activity 也就不会出现内存泄漏问题。
- 如果你需要在 Handler 内部调用外部 Activity 的方法,你可以让这个 Handler 持有这 个 Activity 的弱引用,这样便不会出现内存泄漏的问题了。
- 另外,对于匿名类 Runnable,我们同样可以设置成静态的,因为静态内部类不会持 有外部类的引用。
- 注意:如果使用 Handler 发送循环消息,最好是在 Activity 的 OnDestroy 方法中调用 mLeakHandler.removeCallbacksAndMessages(null);移除消息。(这不是解决内存泄漏 的方法)
5.两种解决办法如下:
弱引用(WeakReference)
public class SampleActivity extends Activity {
/*** Instances of static inner classes do not hold an implicit
- reference to their outer class.
- 弱引用的方式
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity); }
@Override public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
//to Something
}
}
}
静态//定义成 static 的,因为静态内部类不会持有外部类的引用
private final MyHandler mHandler = new MyHandler(this);
private static final Runnable sRunnable = new Runnable() {
@Override public void run() {//to something} };
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
finish(); }}
3.为何主线程可以 new Handler?如果想要在子线程中 new Handler 要做些什么 准备?
每一个 handler 必须要对应一个 looper,主线程会自动创建 Looper 对象,不需要 我们手动创建,所以主线程可以直接创建 handler。 在 new handler 的时候没有传入指定的 looper 就会默认绑定当前创建 handler 的 线程的 looper,如果没有 looper 就报错。 因为在主线程中,Activity 内部包含一个 Looper 对象,它会自动管理 Looper,处 理子线程中发送过来的消息。而对于子线程而言,没有任何对象帮助我们维护 Looper 对象,所以需要我们自己手动维护。 所以要在子线程开启 Handler 要先创建 Looper,并开启 Looper 循环
如果在子线程中创建了一个 Handler,那么就必须做三个操作:
- prepare();
- loop();
- quit();
4.子线程中维护的 Looper,消息队列无消息的时候的处理方案是什么?有什么 用?
在 Handler 机制里面有一个 Looper,在 Looper 机制里面有一个函数,叫做 quitSafely()和 quit()函数,这两个函数是调用的 MessageQueue 的 quit()。
/**
* Quits the looper. * <p>
* Causes the {@link #loop} method to terminate without processing any
* more messages in the message queue.
* Any attempt to post messages to the queue after the looper is asked to quit will fail.
* For example, the {@link Handler#sendMessage(Message)} method will return false.
* </p><p class="note"> * Using this method may be unsafe because some messages may not be delivered
* before the looper terminates. Consider using {@link #quitSafely} instead to ensure
* that all pending work is completed in an orderly manner.
* @see #quitSafely
*/
public void quit() { mQueue.quit(false); }
/*** Quits the looper safely.
* Causes the {@link #loop} method to terminate as soon as all remaining messages
* in the message queue that are already due to be delivered have been handled.
* However pending delayed messages with due times in the future will not be
* delivered before the loop terminates.
* Any attempt to post messages to the queue after the looper is asked to quit will fail.
* For example, the {@link Handler#sendMessage(Message)} method will return false.
*/
public void quitSafely() {
mQueue.quit(true); }
再进入到 MessageQueue 的 quit()函数。
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");}
synchronized (this) {
if (mQuitting) {
return; }
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked(); }
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr); } }
它会 remove 消息,把消息队列中的全部消息给干掉。 把消息全部干掉,也就释放了内存。
private void removeAllFutureMessagesLocked() {
final long now = SystemClock.uptimeMillis();
Message p = mMessages;
if (p != null) {
if (p.when > now) {
removeAllMessagesLocked();
} else { Message n;
for (;;) { n = p.next; if (n == null) {
return; }
if (n.when > now) {
break; }
p = n; }
p.next = null; do {p = n; n = p.next; p.recycleUnchecked(); }
while (n != null); } } }
而在 quit()函数的最后一行,有一个 nativeWake()函数。
// We can assume mPtr != 0 because mQuitting was previously false. nativeWake(mPtr);
这个函数的调用,就会叫醒等待的地方,醒来之后,就接着往下执行。
//native 的方法,在没有消息的时候回阻塞管道读取端,只有 nativePollOnce 返回之后才能往下执行 //阻塞操作,等待 nextPollTimeoutMillis 时长 nativePollOnce(ptr, nextPollTimeoutMillis);
往下执行后,发现 Message msg = mMessages; 是空的,然后就执行了这个,就 接着往下走。
if (msg != null) { ...... } else {
// No more messages.
//没有消息,nextPollTimeoutMillis 复位
nextPollTimeoutMillis = -1; }
然后又调用了这个方法,并且 return 了 null。
// Process the quit message now that all pending messages have been handled.
//如果消息队列正在处于退出状态返回 null,调用 dispose(); 释放该消息队列if (mQuitting) { dispose(); return null; } 所以说,这个时候 Looper 就结束了(跳出了死循环),则达成了第二个作用:释放 线程。
4.既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个 Handler 可能处于不同线程),那它内部是如何确保线程安全的?
这里主要关注 MessageQueue 的消息存取即可,看源码内部的话,在往消息队 列里面存储消息时,会拿当前的 MessageQueue 对象作为锁对象,这样通过加 锁就可以确保操作的原子性和可见性了。 消息的读取也是同理,也会拿当前的 MessageQueue 对象作为锁对象,来保证 多线程读写的一个安全性。
5.我们使用 Message 时应该如何创建它?
创建的它的方式有两种: 一种是直接 new 一个 Message 对象, 另一种是通过调用 Message.obtain() 的方式去复用一个已经被回收的 Message, 当然日常使用者是推荐使用后者来拿到一个 Message,因为不断的去创建新对象 的话,可能会导致垃圾回收区域中新生代被占满,从而触发 GC。
Message 中的 sPool 就是用来存放被回收的 Message,当我们调用 obtain 后, 会先查看是否有可复用的对象,如果真的没有才会去创建一个新的 Message 对 象。
补充:主要的 Message 回收时机是:
1.在 MQ 中 remove Message 后;
2.单次 loop 结束后;
3.我们主动调用 Message 的 recycle 方法后
6.Looper 死循环为什么不会导致应用卡死?
Launch 桌面的图标第一次启动 Activity 时,会最终走到 ActivityThread 的 main 方 法,在 main 方法里面创建 Looper 和 MessageQueue 处理主线程的消息,然后 Looper.loop()方法进入死循环,我们的 Activity 的生命周期都是通过 Handler 机制 处理的,包括 onCreate、onResume 等方法,下面是 loop 方法循环。
主线程的主要方法就是消息循环,一旦退出消息循环,那么你的应用也就退出了, Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞, 能一直处理事件就不会产生 ANR 异常。
造成 ANR 的不是主线程阻塞,而是主线程的 Looper 消息处理过程发生了任务阻 塞,无法响应手势操作,不能及时刷新 UI。
阻塞与程序无响应没有必然关系,虽然主线程在没有消息可处理的时候是阻塞 的,但是只要保证有消息的时候能够立刻处理,程序是不会无响应的。