第二篇 Handler机制
如果问我21世纪最大的进步是什么,我觉得是便捷的生活,通过手机我们可以直接下单想要购买的商品,然后静等快递小哥把它们送到家门,我们只要签收即可,电饭煲让我们摆脱旧时代的柴火,无需旁人照看,就可以在煮好饭的时候提醒你,待你忙完手头的工作,就能直接享用香喷喷的米饭了。总而言之,原来一个人需要去做的各类事情,我们把它剥离出去,除了吃饭睡觉这些必须自己亲自操作的事情,其他你都可以让别人或者一些家居家电来实现。
为了能让你知道米饭已经做好了,快递已经送达了,都需要通过某种发生通知你,Handler在Android的线程中正是充当这么一个角色,因为你不可能在送快递的同时又在家烧火做饭,所以你把这些活都派给了其他人,这些人就是多线程,那么如何知道他们的工作进度或工作结果呢?比如还有多久饭才煮好,快递已经送达哪里?Google的解决方案让你可以给这些线程派一个经理人(Handler),通过他可以让你在需要的时候了解他们的情况而不需要时刻关注。
概述
如果说Android整个应用进程是一个公司的话,每个线程都代表其中的部门或者其他岗位,而主线程就是公司总裁(CEO),我们可以叫他U总(主线程 一般也叫UI线程),U总自然是负责处理一些重要的工作,包括一些很重要的业务决策,这些工作都是比较直观且一般需要快速决定的。(主线程 控制UI界面的显示,且不能做耗时操作)。U总事务繁忙,所以不是什么活都做,所以他开始招人(创建线程),他先招了个小秘和自己一起工作(此时handler绑定的looper是主线程所在的looper),然后又招了几个程序员帮自己干活(线程),虽然U总很忙,但也经常让小秘去跟进进度,当程序员门完成到了某个进度时就将进度成果发送到小秘的微信上,小秘刷着微信,一收到程序员发来的消息里面记录的成果(MessageQueue 消息队列),就会依次把收到的的项目名称(msg.what)的项目进度(msg.obj)告诉U总,一旦项目弄完,U总手头又没有其他事就可以拿着项目成果去和客户洽谈(界面展示)。
让主线程不去做这些耗时操作,且能将子线程里的最终成果告诉主线程,这就是Handler的主要目的。如果你认为线程间需要通信时,就可以通过handler去实现,无论是主线程和子线程间的,还是子线程和子线程间的,都可以通过handler进行通信。但一个handler只能绑定一个线程,就像上面提到的小秘只为U总服务一样,因为也给别人划分业务区吧,就像不同部门的经理,他们之间可以有业务数据的往来和合作,但实际上还是属于不同的工作区域。
角色表
角色 | 真实身份 | 作用 | 备注 |
---|---|---|---|
U总 | 主线程 一般也叫UI线程 | 主要处理一些Ui事件和负责Broadcast消息的接收,同时也能创建子线程来处理其他工作,如果主线程处理耗时任务,当用户进行ui交互后在超过5秒后未处理,此时会触发ANR | 启动应用程序自动开启的主线程 |
程序员 | Thread 子线程 | 主要负责处理一些耗时的工作 如网络请求,文件读写 | 手动开启的线程 |
小秘 | Handler 处理者 | 负责线程间的消息传递 ,能够将Message传到消息队列MessageQueue 处理通过Looper发送来的消息 |
一个线程可以有多个Handler |
小秘的工作汇报 | Looper 循环器 | 将在主线程绑定的消息队列的消息进行循环取出 | 同时一个线程只有一个looper |
小秘的微信 | MessageQueue 消息队列 | 负责存放各个线程推送来的消息Message | - |
小秘收到微信消息 | Message 消息 | 线程间通讯的数据单元,存储要通信的内容 | - |
主线程Handler业务逻辑图
结合概述里的描述,我将主线程里的Handler的主要工作进行下方的流程进行描述。
根据这个流程图我们可以将主要工作分为如下的步骤:
1、异步线程通信前准备
此步骤主要在主线程中对Looper对象(处理器),Handler对象,MessageQueue对象(消息队列)的创建,而这些都是主线程相关对象。
2、消息循环和消息发送
子线程通过Handler将消息Message对象发送到主线程绑定的MessageQueue中,同时Looper会不断循环消息队列,一旦从里面取出消息就会发送给创建该消息的处理者(Handler),Handler在handleMessage方法中进行对应的处理操作
3、消息处理
Handler在handleMessage方法中进行对应的处理操作,如果是在主线程创建的Handler,可以执行UI操作。
子线程Handler逻辑图
Handler处理可以将子线程里的消息传递给主线程,子线程间也可以通过Handler互相通信,如下图所示:
由上图我们需要注意以下几点关系:
- 1个线程(Thread)只能绑定 1个循环器(Looper),但可以有多个处理者(Handler)
- 1个循环器(Looper) 可绑定多个处理者(Handler),这些Handler可以是不同线程的
- 1个处理者(Handler) 只能绑定1个1个循环器(Looper)
- Handler发送消息的MessageQueue得看创建的时候实际绑定的Looper
Tips:多个线程Handler可以往一个Looper所持有的MessageQueue中发送消息,但Looper所要分发消息的Handler是原先发送消息的,也就是自己发送的消息到最后实际上也是自己处理,原因可看下方小知识。
Handler使用
下面简单回顾下,一个Handler可以如何使用:
1、创建Handler 和 消息处理
通过继承Handler和复写handleMessage方法可实现消息接收后的处理
其中创建的handler实例的作用域,需要包含需要发送消息的线程,否则无效进行消息发送。
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);// 需执行的操作
}
};
2、从线程中发出消息
Message message=handler.obtainMessage();
message.what=100;
message.arg1=i;
message.obj="消息内容";
handler.sendMessage(message);
其中Message对象
属性名 | 类型 | 作用 |
---|---|---|
message.what | int类型 | 可用来进行发送消息类型的识别 |
message.arg | int类型 | 可用来传递int型的消息内容,arg1 arg2都可赋值,占用空间会相较于obj小 |
message.obj | Object类型 | 可用来传递各种类型的消息内容 |
3、使用post发送和处理消息
除了sendMessage方法,我们也可以通过post分发进行消息发送,只不过post方法是将runner封装成Messgae进行发送和立即处理,其中的工作原理其实是相似的。
mHandler.post(new Runnable() {
@Override
public void run() {
... // 需执行的操作
}
});
小知识
1、Looper如何判断Message由哪个Handler处理?
通过下方路径进入mHandler.sendMessage(msg)内部的源码里,我们可以发现在实际发送消息时,Message.target为把自己作为属性进行引用传递,Looper在循环到Message消息时会通过该target向指定Handler进行消息分发。
方法路径:
sendMessage(msg)
– sendMessageDelayed(Message msg, long delayMillis)
— sendMessageAtTime(Message msg, long uptimeMillis)
---- enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// 1. 将msg.target赋值为this
// 把当前的Handler实例对象作为msg的target属性
msg.target = this;
// Looper的loop()在消息循环时,会从消息队列中取出每个消息msg,然后执行msg.target.dispatchMessage(msg)去处理消息
// 实际上则是将该消息派发给对应的Handler实例
// 2. 调用消息队列的enqueueMessage()
// 即:Handler发送的消息,最终是保存到消息队列
return queue.enqueueMessage(msg, uptimeMillis);
}
2、Handler 的内存泄露
当一个对象已经不再使用了,本该被回收时,而有另一个正在使用的对象持有它的引用从而导致对象不再被回收。这种导致了本该被回收的对象而停留在堆内存中,就产生了内存泄漏。
泄露原因
当MessageQueue 存在Message没有被释放时(未处理的消息 / 正在处理消息时),说明有Handler的实例在被Message持有,持有原因由第一个知识点中的Message会把当前的Handler实例对象作为msg的target属性就可以知道。
由于Handler = 非静态内部类 / 匿名内部类(2种使用方式),故又默认持有外部类的引用(即Activity实例),一旦Activity要被回收时,而Handler实例却停留在堆内存中,因此就导致了activity无法回收,进而导致内存泄漏。
解决方案
1、使用静态内部类
原理:静态内部类不默认持有外部类的引用,从而使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 不存在。
具体方案:将Handler的子类设置成静态内部类。此外,还可使用WeakReference弱引用持有外部类,保证外部类能被回收。因为:弱引用的对象拥有短暂的生命周期,在垃圾回收器线程扫描时,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
解决代码:
// 设置为:静态内部类
private static class SHandler extends Handler{
// 定义 弱引用实例
private WeakReference<Activity> reference;
// 在构造方法中传入需持有的Activity实例
public SHandler(Activity activity) {
// 使用WeakReference弱引用持有Activity实例
reference = new WeakReference<Activity>(activity); }
@Override
public void handleMessage(Message msg) {
}
}
2、利用外部类的生命周期,清空Handler内消息队列
原理:不仅使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 不复存在,同时 使得 Handler的生命周期(即 消息存在的时期) 与 外部类的生命周期 同步
具体方案:当 外部类(此处以Activity为例) 结束生命周期时(此时系统会调用onDestroy()),清除 Handler消息队列里的所有消息(调用removeCallbacksAndMessages(null))
解决代码:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
// 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
}
3、快速切换到主线程执行的方案
1、通过创建绑定主线程的Looper的Handler来切换
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
//此时已在主线程中
}
});
2、 activity.runOnUiThread(Runnable action)方法
public void updateOnUI(Activity activity) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
//此时已在主线程中
}
});
}
3、view.post(Runnable action)
public void updateOnUI(View view) {
view.post(new Runnable() {
@Override
public void run() {
//此时已在主线程中
}
});
}
可能遇到的相关问题
1、handler.sendMessage()与handler.post()的区别?
查看Handler的源码可以发现,最终发送至MessageQueue的方法都是Handler中的enqueueMessage(),但是对于Message的处理,在分发时,会将消息的结果返回至其callback 中。
2、一个线程可以有几个Handler?几个Looper?几个MessageQueue?
Handler:多个,由于通过Message的target方法在添加和分发时标注Handler,实现准确的分发,因此同一个线程可以创建多个Handler对象.
Looper:一个,获取Looper需要先调用Looper.prepare()方法,查看下面源码可以发现,如果包含多个Looper对象会抛出异常。
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));
}
MessageQueue:一个,在调用Looper.prepare()时,会判断该线程的Looper是否为空,只有为空的情况才会调用Looper的构造方法,创建MessageQueue,因此只有一个。
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
面试技巧之自我介绍
面试开头让面试者先进行自我介绍已经是固定的流程和步骤了,可在这方面我们可以通过以下几点来提升我们竞争能力:
1、自我介绍时间控制在1-3分钟,注意不要超过3分钟
2、不要只说形容词而没有具体能体现的经历,将自己的经历用数据,成效来表现,比如表达自己的技术能力,可以以时间,技术栈,成效作为关键字来进行描述,如用谷歌CameraX和Detect,在3天左右的时间,封装了一个人脸识别的sdk,包括多人脸和单人脸的展现形式和可以用来作为识别人脸封装的View。
3、把自己的优势说出来,尽量往岗位需求靠拢
4、面试可以适度进行对自己进行包装,但切记不要撒谎