找工作面试中这个问题被问到的概率至少是80%,跟面试官扯一扯Handler消息的原理,会让面试加分很多的。最近我也在找工作,把这个知识点总结下,以备面试之需。
为什么使用Handler机制?
简而言之:Android中主线程不能执行耗时操作,会导致ANR异常,所有请求网络、数据大批量操作都在子线程中,但是子线程又不能更新UI,所以Google为我们分装了Handler消息机制。
Android 中主线程也叫 UI 线程,那么从名字上我们也知道主线程主要是用来创建、更新 UI 的,而其他耗时操作,比如网络访问,或者文件处理,多媒体处理等都需要在子线程中操作,之所以在子线程中操作是为了保证 UI 的流畅程度,手机显示的刷新频率是 60Hz,也就是一秒钟刷新 60 次,每 16.67 毫秒刷新一次,为了不丢帧,那么主线程处理代码最好不要超过 16 毫秒。当子线程处理完数据后,为了防止 UI 处理逻辑的混乱,Android 只允许主线程修改 UI,那么这时候就需要 Handler 来充当子线程和主线程之间的桥梁了。
1. Handler的解释
Handler是Android官方给我们提供的一套更新UI线程的机制,也是一套消息处理机制,可以通过Handler来处理消息,更新UI等。
2. Handler机制中的四个类
说到Handler消息机制,我们首先要提到的就是这四个主要类:
2.1 Message
Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。
Message类的obtain方法:
消息队列顺序的维护是使用单链表的形式来维护的;
把消息池里的第一条数据取出来,然后把第二条变成第一条。
2.2 MessageQueue
MessageQueue主要包括两个操作:插入和读取
读取操作本身会伴随这删除操作
插入和读取对应的方法分别为enqueueMessage和next
它的内部实现并不是队列,而是一个单链表的数据结构来维护消息列表,单链表在插入和删除上比较有优势
2.3 Looper
Looper扮演着消息循环的角色
Looper除了prepare方法外,还提供了prepareMainLooper方法,这个方法主要是给主线程也就是ActivityThread创建Looper使用的
Looper提供了quit和quitSafely来退出一个Looper
Looper的loop方法是一个死循环,唯一跳出循环的方式是MessageQueue的next方法返回null(当Looper的quit方法被调用时,Looper就会调用MessageQueue的quit或quitSafely方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null)
Looper处理消息的流程
//获取消息队列的消息
Message msg = queue.next(); // might block
...
//分发消息,消息由哪个handler对象创建,则由它分发,并由它的handlerMessage处理
msg.target.dispatchMessage(msg);
这里的dispatch是在Looper中执行的,这样就成功将代码逻辑切换到了指定的线程中去执行
主线程Looper从消息队列读取消息,当读完所有消息时,进入睡眠,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠
2.4 Handler
Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前没有Looper则报错
Handler通过post或send将msg投递到MessageQueue中
Handler发送消息,sendMessage的所有重载,实际最终都调用了sendMessageAtTime
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
Looper轮询消息队列,并且运行在创建Handler的线程中, 这样一来Handler中的业务逻辑就被切换到创建Handler所在的线程中执行了
创建Handler对象时,在构造方法中会获取Looper和MessageQueue的对象
2.5 HandlerThread
HandlerThread是Android官方给我们提供好的一套子线程的Handler,也就是异步处理机制,它是为了避免线程切换导致空指针异常的错误。
2.6 ThreadLocal
ThreadLocal是一个线程内部的数据存储类, 通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据
Andorid源码中Looper、ActivityThread以及AMS中都用到了ThreadLocal
ThreadLocal的另一个使用场景是复杂逻辑下的对象传递:比如监听器的传递
原理分析
ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找对应的value值
内部实现
set方法
1、通过values方法来获取当前线程中的ThreadLocal数据,没有localValues则初始化
2、然后再将ThreadLocal的值进行存储
get方法
1、取出当前线程的localValues对象,没有则返回初始值
2、再取出它的table数组并找出ThreadLocal的reference对象在table数组中的位置
3、然后table数组的下一个位置所存储的数据就是ThreadLocal的值
从set和get可以看出,它们操作的对象都是当前线程的localValues对象的table数组,因此你不同线程中访问同一个ThreadLocal的set和get方法,它们对ThreadLocal所做的读/写操作仅限于各自线程的内部。
2.6 主线程的消息循环
Android的主线程就是ActivityThread,主线程的入口是main,在main方法中系统会通过Looper.prepareMainLooper()来创建主线程的Looper和一个MessageQueue,并通过Looper.loop()来开启主线程的消息循环。
ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityThread中去执行,即切换到主线程中去执行。
3.内部循环原理解析
在主线程中 Android 默认已经调用了 Looper.preper()方法,调用该方法的目的是在 Looper 中创建MessageQueue 成员变量并把 Looper 对象绑定到当前线程中。
当调用 Handler 的 sendMessage(对象)方法的时候就将 Message 对象添加到了 Looper 创建的 MessageQueue 队列中,同时给 Message 指定了 target 对象,其实这个 target 对象就是 Handler 对象。主线程默认执行了 Looper.looper()方法,该方法从 Looper 的成员变量MessageQueue 中取出 Message,然后调用 Message 的 target 对象的handleMessage()方法。这样就完成了整个消息机制。
4. AsyncTask简介
AsyncTask 是一个抽象类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为 AsyncTask 类指定三个泛型参数,这三个参数的用途如下。
1. Params
在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。
2. Progress
后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
3. Result
当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值
类型。
《第一行代码》
AsyncTask 中的经常需要去重写的方法有以下四个。
1. onPreExecute()
这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比
如显示一个进度条对话框等。
2. doInBackground(Params...)
这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任
务。任务一旦完成就可以通过 return 语句来将任务的执行结果返回,如果 AsyncTask 的
第三个泛型参数指定的是 Void,就可以不返回任务执行结果。注意,在这个方法中是不
可以进行 UI 操作的,如果需要更新 UI元素,比如说反馈当前任务的执行进度,可以调
用 publishProgress(Progress...)方法来完成。
3. onProgressUpdate(Progress...)
当在后台任务中调用了 publishProgress(Progress...)方法后,这个方法就会很快被调
用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对 UI 进行操
作,利用参数中的数值就可以对界面元素进行相应地更新。
4. onPostExecute(Result)
当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返
回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些 UI 操作,比如
说提醒任务执行的结果,以及关闭掉进度条对话框等。
如果想要启动这个任务,只需编写以下代码即可:
new DownloadTask().execute();
《第一行代码》
5. 面试问答题
Handler和AsyncTask的区别
这俩类都是用来实现异步的,其中AsyncTask的集成度较高,使用简单,Handler则需要手动写Runnable或者Thread的代码;
另外,由于AsyncTask内部实现了一个非常简单的线程池,实际上是只适用于轻量级的异步操作的,一般不应该用于网络操作和数据库的大量操作。
只能在UI线程里面更新界面吗?
答:不一定,之所以子线程不能更新界面,是因为Android在线程的方法里面采用checkThread进行判断是否是主线程,而这个方法是在ViewRootImpl中的,这个类是在onResume里面才生成的,因此,如果这个时候子线程在onCreate方法里面生成更新UI,而且没有做阻塞,就是耗时多的操作,还是可以更新UI的。
Android子线程更新UI的方式有几种?
答:一般情况下,我们都采用Handler的方式进行更新UI,当然,代码层的实现有不同的方法,比如可以使用Handler的post方法进行更新UI,或者用Handler的sendMessage方法进行更新UI,或者通过View的post方法进行更新,还有一个是runOnUIThread也是可以进行更新的。但这些本质上还是通过Handler进行子线程的更新。
使用Handler的时候一般会遇到什么问题?
答:比如说子线程更新UI,是因为触发了checkThread方法检查是否在主线程更新UI,还有就是子线程中没有Looper,这个原因是因为Handler的机制引起的,因为Handler发送Message的时候,需要将Message放到MessageQueue里面,而这个时候如果没有Looper的话,就无法循环输出MessageQueue了,这个时候就会报Looper为空的错误。