Android 中的 Handler 是用来和线程通信的重要工具。它主要用于在后台线程中执行任务,并将结果传递回主线程以更新用户界面。
一、基本概念
- 线程间通信: Android 应用通常具有主线程(也称为 UI 线程)和后台线程。Handler 允许您从后台线程向主线程发送消息,从而在更新 UI 时避免出现线程安全问题。
- 消息队列: Handler 使用消息队列来存储消息。每个消息都可以包含一个数据包,用于在线程之间传递信息。
Handler 不仅仅用于将消息发送到主线程,您还可以在不同的线程之间进行通信。您可以在某个线程上创建 Handler,并将其传递给其他线程以便它们能够向该 Handler 发送消息。
1、使用案例
- 在后台线程执行任务后更新 UI。
- 在网络请求完成后通知 UI 更新。
- 实现定时器功能,定期执行某个任务。
2、注意事项
- Handler 与线程安全相关。确保您理解线程之间的通信机制以避免出现竞态条件或死锁。
- 避免在 Handler 中进行耗时操作,以免阻塞主线程。
总的来说,Handler 是 Android 开发中非常有用的工具,用于实现多线程通信和管理 UI 线程的消息处理。它为开发者提供了一种简单而有效的方法来处理异步任务和更新用户界面。
二、使用方法
1、创建Handler
您可以通过以下方式创建一个 Handler:
Handler handler = new Handler();
这将创建一个与当前线程的 Looper 相关联的 Handler。
2、发送消息
要向 Handler 发送消息,可以使用以下方法:
handler.sendMessage(Message msg);
其中 Message
对象包含您要传递的信息。
3、处理消息
要在 Handler 中处理消息,您需要重写 handleMessage(Message msg) 方法。例如:
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理收到的消息
}
};
4、延迟消息
Handler 还允许您发送延迟消息,可以使用以下方法:
handler.sendMessageDelayed(Message msg, long delayMillis);
这样可以在指定的延迟时间后将消息发送到消息队列中。
5、移除消息
有时候您可能需要在某个时刻取消将要发送的消息。您可以使用以下方法:
handler.removeMessages(int what);
这将移除具有特定标识符的所有消息。
三、子线程创建Handler
我们平时使用 Handler 都是在主线程中 new Handler(),那么在子线程中也可以这样创建Handler吗?
private Handler threadHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
threadHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
return false;
}
});
}
}).start();
}
了解 Handler 原理的都知道,Handler 的消息处理是通过 Looper.loop() 里的死循环,不断的从消息队列中取出消息并处理,在主线程中已经自动调用了 Looper.loop() 方法,所以我们可以直接使用 new Handler() 创建,而在子线程中需要我们手动创建。
private Handler threadHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
threadHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
return false;
}
});
Looper.loop();
}
}).start();
}
上面的代码没有问题,但我们都是使用 handler.sendManager() 发送消息,在其他地方我们怎么创建该子线程对应的 handler 去发送消息呢?因为无法拿到子线程的 Looper,所以无法创建。
下面我们创建一个自定义子线程,提供获取 Looper 方法:
public class MyHandlerThread extends Thread {
Looper looper;
MyHandlerThread(String name){
super(name);
}
@Override
public void run() {
super.run();
Looper.prepare();
looper = Looper.myLooper();
Looper.loop();
}
Looper getLooper(){
return looper;
}
@Override
public synchronized void start() {
super.start();
}
}
使用自定义 Thread 创建 handler。
MyHandlerThread handlerThread = new MyHandlerThread("cx");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
//子线程使用完成一定要退出,防止内存溢出
handler.getLooper().quit();
上面方法中 handlerThread.getLooper() 可能存在空指针异常,以为 run() 为异步加载。所以必须要保证 run() 执行完,再去执行 getLooper() 方法,可以使用 wait() 和 notify() 实现。
public class MyHandlerThread extends Thread {
Looper looper;
MyHandlerThread(String name){
super(name);
}
@Override
public void run() {
super.run();
Looper.prepare();
synchronized (this) {
looper = Looper.myLooper();
notifyAll(); //唤醒wait()
}
Looper.loop();
}
Looper getLooper(){
synchronized (this) {
while (looper == null) {//用while,防止被其他notify唤醒,重新判断
try {
wait(); //释放锁,并等待
} catch (InterruptedException e) {
}
}
}
return looper;
}
@Override
public synchronized void start() {
super.start();
}
}
wait():CPU 释放,线程继续做别的事情。
sleep():睡眠,线程停止等待。
上面主要是为了理解 wait()、notify() 和 synchronized() 的使用,其实 MyHandlerThread 已经帮我们实现好了,直接使用 HandlerThread 即可。
四、Handler相关问答
1、用一句话概括Handler,并简述其原理
Handler 是 Android 系统的根本,在 Android 应用被启动的时候,会分配一个单独的虚拟机,虚拟机会执行 ActivityThread 中的 main 方法,在 main 方法中对主线程 Looper 进行了初始化,也就是几乎所有代码都执行在 Handler 内部。Handler 也可以作为主线程和子线程通讯的桥梁。 Handler 通过 sendMessage 发送消息,将消息放入 MessageQueue 中,在 MessageQueue 中通过时间的维度来进行排序,Looper 通过调用 loop 方法不断的从 MessageQueue 中获取消息,执行 Handler 的 dispatchMessage,最后调用 handleMessage 方法。
2、为什么系统不建议在子线程访问UI(为什么不能在子线程更新UI)
在某些情况下,在子线程中是可以更新 UI 的。但是在 ViewRootImpl 中对 UI 操作进行了 checkThread,但是我们在 onCreate 和 onResume 中可以使用子线程更新UI,由于我们在 ActivityThread 中的 performResumeActivity 方法中通过 addView 创建了 ViewRootImpl,这个行为是在 onResume 之后调用的,所以在 onCreate 和 onResume 可以进行更新UI。
但是我们不能在子线程中更新 UI,因为 UI 更新本身是线程不安全的,如果添加了耗时操作之后,一旦 ViewRootImpl 被创建将会抛出异常。一旦在子线程中更新 UI,容易产生并发问题。
3、一个Thread可以有几个Looper,几个Handler
一个线程只能有一个 Looper,可以有多个 Handler。
- 一个Looper:因为Looper需要循环,循环后面的代码无法执行了,所以一个线程只有一个Looper
- 多个Handler:为了相互独立,互不干扰,各自处理各自的消息,谁发生的Message谁处理,也可以通过removeMessages清空掉自己的Message。
线程在初始化时候调用 Looper.prepare() 方法对将静态对象 ThreadLocal 作为 key(整个进程全部线程的 Looper 共用一个 sThreadLocal),创建 Looper 对象作为 value 存储在当前线程的 ThreadLocalMap 中(其实就只有一个 key 对应 value,key 为 sThreadLocal、value 为 looper 对象。这两只有一个,所以 ThreadLocalMap 只有一个键值对)。
通过线程独有的 ThreadLocal 实现将 Looper 存储在 ThreadLocalMap 这样键值对的数据结构中。
4、Message是如何创建的
首先考虑一个问题,屏幕刷新率 60Hz(即每秒刷新60次),每次刷新要用到 3 个 Message,也就是每秒钟至少要创建 180 个 Message。这样不断的创建回收,就会出现内存抖动的问题,从而导致 GC、屏幕卡顿等问题。
为了解决上面的问题,采用 Message 了享元的设计模式,使用 obtain() 方法创建。在Handler 中创建两个线程池队列,一个是我们比较熟悉的 MessageQueue,另一个就是回收池 sPool(最大长度是 50 复用池)。MessageQueue 中 Message 回收时,我们将清空数据的 Message 放回到 sPool 队列中。创建 Manager,我们直接从 sPool 池中取出来就可以了。
应用场景:地图、股票、RecyclerView复用等对数据的处理都使用了享元模式。
5、主线程中Looper的轮询死循环为何没有阻塞主线程
Looper 轮询是死循环,但是当没有消息的时候他会 block(阻塞, 阻塞代码没有执行计时操作),ANR 是当我们处理点击事件的时候 5s 内没有响应,我们在处理点击事件的时候也是用的 Handler,所以一定会有消息执行,并且 ANR 也会发送 Handler 消息,所以不会阻塞主线程。Looper 是通过 Linux 系统的 epoll 实现的阻塞式等待消息执行(有消息执行无消息阻塞),而 ANR 是消息处理耗时太长导致无法执行剩下需要被执行的消息触发了 ANR。
6、ANR机制
启动 Service 为例,APP进程 startService,system_service 进程通知 ActivityManager开始计时,同时 Service 进程开始启动,启动完成后会发送消息到 ActivityManager。
如果时前台 Service,ActivityManager 20秒没有收到启动完成的消息就会产生ANR。
7、使用Hanlder的postDealy()后消息队列会发生什么变化
Handler 发送消息到消息队列,消息队列是一个时间优先级队列,内部是一个单向链表。发动 postDelay 之后会将该消息进行时间排序存放到消息队列中。
8、点击页面上的按钮后更新TextView的内容,谈谈理解
点击按钮的时候会发送消息到 Handler,但是为了保证优先执行,会加一个标记异步,同时会发送一个 target 为 null 的消息,这样在使用消息队列的 next 获取消息的时候,如果发现消息的 target 为 null,那么会遍历消息队列将有异步标记的消息获取出来优先执行,执行完之后会将 target 为 null 的消息移除。(同步屏障:拦截同步消息执行,优先执行异步消息。 View 更新时,draw、requestLayout、invalidate 等很多地方都调用异步消息的方法)。
同步屏障的设置可以方便地处理那些优先级较高的异步消息。当我们调用 Handler.getLooper().getQueue().postSyncBarrier() 并设置消息的setAsynchronous(true)时,target 即为 null ,也就开启了同步屏障。当在消息轮询器 Looper 在 loop() 中循环处理消息时,如若开启了同步屏障,会优先处理其中的异步消息,而阻碍同步消息。
9、生产者-消费者设计模式
在生产者-消费者模式中如果消费者消费能力大于生产者,或者生产者生产能力大于消费者,需要一个限制,在 java 里有一个 blockingQueue。当目前容器内没有东西的时候,消费者来消费的时候会被阻塞,当容器满了的时候也会被阻塞。
这里 Handler.sendMessage 相当于一个生产者,MessageQueue 相当于容器,Looper 相当于消费者。Handler 不使用 java 的 blockingQueue 的原因是除了我们上层需要使用到 Handler,其实底层的消息都是需要传递给 Handler 处理,比如:驱动层需要发事件给APP、屏幕点击事件、底层刷新通知等,所以我们使用的是 Native 层提供的 MessageQueue 实现消息队列。
10、Handler是如何完成子线程和主线程通信的
在主线程中创建 Handler,在子线程中发送消息,放入到 MessageQueue 中,通过 Looper.loop 取出消息进行执行 handleMessage,由于 looper 我们是在主线程初始化的,在初始化 looper 的时候会创建消息队列,所以消息是在主线程被执行的。
11、关于ThreadLocal
ThreadLocal 实例进程内只有一个(静态实例),但其内部的 set 和 get 方法是获取的当前线程的 ThreadLocalMap 对象,ThreadLocalMap 是每个线程有一个单独的内存空间,不共享,ThreadLocal 在 set 的时候会将数据存入对应线程的 ThreadLocalMap中,key = ThreadLocal,value = 值。
12、Handler内存泄漏问题及解决方案
内部类持有外部类的引用导致了内存泄漏,如果 Activity 退出的时候,MessageQueue中还有一个 Message 没有执行,这个 Message 持有了 Handler 的引用,而 Handler 持有了 Activity 的引用,导致 Activity 无法被回收,导致内存泄漏。使用 static 关键字修饰,在 onDestory 的时候将消息清除。
简单理解,当 Handler 为非静态内部类时,其持有外部类 Actviity 对象,所以导致 static Looper -> mMainLooper -> MessageQueue -> Message -> Handler -> MainActivity,这个引用链中 Message 如果还在 MessageQueue 中等待执行,则会导致 Activity 一直无法被释放和回收。
导致的原因是因为 Looper 需要循环,所以一个线程只有一个 Looper,但一个线程中可有多个 Handler,MessageQueue 中消息 Message 执行时不知道要通知哪个 Handler 执行任务,所以在 Message 创建时中存入了 Handler 对象 target 用于回调执行的消息。如果 Handler 是 Activity 这种短生命周期对象的非静态内部类时,则会让创建出来的 Handler 对象持有该外部类 Activity 的引用,Message 还在队列中导致引用着 Handler,而非静态内部类 Handler 引用外部类 Activity 导致 Activity 无法被回收,最终导致内存泄漏。
解决办法:
- Handler 不能是 Activity 这种短生命周期的对象类的内部类;
- 在 Activity 销毁时,将创建的 Handler 中的消息队列清空并结束所有任务。
- 将 handler 设置成 static,static 变量是全局变量,不能够自动引用外部类变量,这时Handler 就不再持有 MainActivity,MainActivity 就可以正常释放。
13、Handler异步消息处理(HandlerThread)
内部使用了 Handler+Thread,并且处理了getLooper 的并发问题。如果获取 Looper 的时候发现 Looper 还没创建,则 wait,等待 looper 创建了之后在 notify。
14、子线程中的Looper,无消息时如何处理
子线程中创建了 Looper,当没有消息的时候子线程将会被 block,无法被回收,所以我们需要手动调用 quit 方法将消息删除并且唤醒 looper,然后 next 方法返回 null 退出 loop。
15、多个Handler往MessageQueue中添加数据(Handler可能处于不同线程),如何确保线程安全
在添加数据和执行 next 的时候都加了 this 锁,这样可以保证添加的位置是正确的,获取的也会是最前面的。
16、关于IntentService
HandlerThread + Service 实现,可以实现 Service 在子线程中执行耗时操作,并且执行完耗时操作时候会将自己 stop。
17、Glide如何维护生命周期
一般想问的应该都是这里为什么会判断两次 null :
@NonNull
private RequestManagerFragment getRequestManagerFragment(@NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) {
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
current = pendingRequestManagerFragments.get(fm);
if (current == null) {
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
if (isParentVisible) {
current.getGlideLifecycle().onStart();
}
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}
1)在多次调用 with 的时候,commitAllowingStateLoss 会被执行两次,所以我们需要使用一个 map 集合来判断,如果 map 中已经有了证明已经添加过了。
2)handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(),我们需要将 map 里面的记录删除。
18、handler主线程阻塞了怎么办
Android 系统事件驱动系统,loop 循环处理事件,如果不循环程序就结束了。
19、Handler底层为什么用epoll,为什么不用select和poll
Socket 非阻塞 IO 中 select 需要全部轮询不适合,poll 也是需要遍历和 copy,效率太低了。epoll 非阻塞式 IO,使用句柄获取 APP 对象,epoll 无遍历,无拷贝。还使用了红黑树(解决查询慢的问题)。