在Android开发的过程中,我们常常会将耗时的一些操作放在子线程(work thread)中去执行,然后将执行的结果告诉UI线程(main thread)。Handler不仅仅能将子线程的数据传递给主线程,它能实现任意两个线程的数据传递。
为什么系统不允许子线程访问UI呢,因为AndroidUI不是线程安全的,如果在多线程操控UI可能会导致UI控件处于不可预知的状态;那为什么系统不给UI控件的访问加锁呢?首先上锁机制会让UI访问的逻辑变得复杂,其次加锁机制会降低访问效率。因此,安卓之所以提供Handler是为了解决在子线程不能更新UI的矛盾。
Handler 架构图
image.png为什么要采用Handler机制
答: 在Android中,只有主线程才能更新UI,但是主线程不能进行耗时操作,否则会产生ANR异常,所以常常把耗时操作放到其他子线程进程。如果在子线程中需要更新UI,一般都是通过Handler发送消息,主线接受消息并进行相应的逻辑处理。
Handler用途
Handler有两个主要的用途
1 安排消息和Runnable对象在未来执行
2 将你的一个动作放在不同的线程上执行
Handler模型
Handler的消息机制主要包含:
- Message:消息实体
- MessageQueue:消息队列
- Handler:消息管理类向消息池发送各种消息事件
- Looper:不断的循环执行(Looper.loop),按分发机制将消息分发给目标处理者
1.一个线程有几个 Handler?
答:一个线程可以用有多 Handler,因为 Handler 最终是被 Message 持用的(post 里面的 Runnable 最终也会被包装成一个 Message),以便 Looper 在拿到 Message 后调用 Handler 的 dispatchMessage 完成回调,而且项目中仔细去看也确实如此,我们可以每个 Activity 中都创建一个 Handler 来处理回调到主线程的任务。
2.一个线程有几个 Looper?如何保证?
答:一个线程只能拥有一个 Looper,这里从源码中就可以看到,sThreadLocal.set 只调用了一次,如果再次调用 prepare 会判断 sThreadLocal.get 是否为空,如果不为空就直接抛出异常了,也就是同一线程多次调用 prepare 方法会直接崩溃,这里也是避免了程序去修改某个线程已经设置好的 Looper 值。
image补充:ThreadLocal 提供线程局部变量,也就是对应值只有该线程支持,并不会多线程共享。那它是如何做到线程局部变量这个效果的呢?它内部靠的是 ThreadLocalMap,线程作为 key,值作为 value,这样我去取对应值的时候,其实通过线程 Key 拿去对应的 value,这样就保证了值是当前线程独享的。
3.为何主线程可以使用 Handler?如果想要在子线程中使用 Handler 机制要做些什么准备?
答:先来看下面的源码,Handler 的构造中(无论调用哪个最终都会走到这里),是需要判断当前线程是否存在 Looper 的,如果不存在会直接抛出异常,主线程之所以可以使用 Handler 是因为系统帮在 ActivityThread 中已经帮我们创建了 Looper 并且已经让它运行了起来。
image系统帮我们在主线程创建 Looper 的代码:
image如果我们现在子线程中使用 Handler 的话,之需要模仿系统怎么创建 Looper 即可,其实就是两步,在子线程中调用 Looper.prepare() 和 Looper.loop() 即可,prepare 帮我们在对应线程创建 Looper,loop 让刚刚创建好的 Looper 运行起来。
这两步完成后我们就可以在子线程中使用 Handler 了。
以上所说的 Handler 使用指的是 Handler 的创建,比如在 A 线程创建后就可以在任何位置使用了,也就是在任意线程发送消息,然后在 A 线程处理消息。
4.既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个 Handler 可能处于不同线程),那它内部是如何确保线程安全的?
答:这里主要关注 MessageQueue 的消息存取即可,看源码内部的话,在往消息队列里面存储消息时,会拿当前的 MessageQueue 对象作为锁对象,这样通过加锁就可以确保操作的原子性和可见性了。
消息的读取也是同理,也会拿当前的 MessageQueue 对象作为锁对象,来保证多线程读写的一个安全性。
image5.我们使用 Message 时应该如何创建它?
答:创建的它的方式有两种,一种是直接 new 一个 Message 对象,另一种是通过调用 Message.obtain() 的方式去复用一个已经被回收的 Message,当然日常使用者是推荐使用后者来拿到一个 Message,因为不断的去创建新对象的话,可能会导致垃圾回收区域中新生代被占满,从而触发 GC。
Message 中的 sPool 就是用来存放被回收的 Message,当我们调用 obtain 后,会先查看是否有可复用的对象,如果真的没有才会去创建一个新的 Message 对象。
补充:主要的 Message 回收时机是:
- 在 MQ 中 remove Message 后;
- 单次 loop 结束后;
- 我们主动调用 Message 的 recycle 方法后;
6.Message 的数据结构是什么样子?
答:单链表,Message 中会通过 next 来持有下一个 Message 对象的引用,这是一个典型的链表结构。
image其实文中写到的这些问题并不只是问到这里就结束了,很多问题都可以深挖,尤其是这个问题,看着很短,接下去问很多关于链表的其他问题了,比如链表反转,或是延伸到其他数据结构问题。
7.主线程 Looper 与子线程 Looper 有什么不同?
答:最主要的区别还在在于 Looper 的 loop 循环是否能够退出,主线程创建时传入的 quitAllowed 是 false。
imageLooper.loop 这个方法在拿到的消息为空时就会退出那个死循环,不过一般是不为空的,哪怕没有消息最多也是阻塞,只有调用 Looper.quit 时才会在消息队列清空消息并把消息设置为 null。
那 loop 的死循环结束意味着什么呢?看下面 ActivityThread 的代码就知道了,如果主线程 Looper 结束就说明程序也要退出了,因为只有 loop 不断执行才不会走到抛出异常那一行。
文件:android/app/ActivityThread.java
public static void main(String[] args) {
// ...
// 主线程 Looper 创建
Looper.prepareMainLooper();
// ...
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
// ...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
8. Looper 死循环为什么不会导致应用卡死?
真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死
答:epoll+pipe,有消息就依次执行,没消息就block住,让出CPU,等有消息了,epoll会往pipe中写一个字符,把主线程从block状态唤起,主线程就继续依次执行消息。
简答:loop死循环不造成ui阻塞 是因为当消息队列无消息时会进入休眠状态 这时候会释放cpu资源,有消息时会被唤醒。
主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
9. 子线程有哪些更新UI的方法。
- 主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI。
- 用Activity对象的runOnUiThread方法。
- 创建Handler,传入getMainLooper。
- View.post(Runnable r) 。
10. 如何处理Handler 使用不当导致的内存泄露?
解决Handler内存泄露主要2点
- 有延时消息,要在Activity销毁的时候移除
Messages
- 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。
11. 消息分发的优先级:
- Message的回调方法:message.callback.run(),优先级最高;
- Handler的回调方法:Handler.mCallback.handleMessage(msg),优先级仅次于1;
- Handler的默认方法:Handler.handleMessage(msg),优先级最低。
12. 消息池
在代码中,可能经常看到recycle()方法,咋一看,可能是在做虚拟机的gc()相关的工作,其实不然,这是用于把消息加入到消息池的作用。这样的好处是,当消息池不为空时,可以直接从消息池中获取Message对象,而不是直接创建,提高效率。
静态变量sPool
的数据类型为Message,通过next成员变量,维护一个消息池;静态变量MAX_POOL_SIZE
代表消息池的可用大小;消息池的默认大小为50。
消息池常用的操作方法是obtain()和recycle()。
13. Handler如何切换线程的
切换线程实际上就是调用位置的切换而已,Handler切换线程的机制最终原因是handleMessage方法的调用位置的切换,比如主线程。
handler将自己的引用间接被Looper持有,当Looper在主线程调用loop()方法时,该方法会取出handler并调用其handleMessage()方法,相当于切换到主线程
MessageQueue内部使用单链表来存储信息Message,Handler发送的Message全部都添加到了MessageQueue中,Looper#loop()方法通过调用MessageQueue#next()方法不断遍历链表中的Message,当取得符合的Message后,通过Message持有的Handler对象引用调用Handler#handleMessage方法,如此,Looper#loop()在哪个线程调用的,handleMessage方法就切换到哪个线程了。
14. 延时消息如何处理
1.消息是通过MessageQueen中的enqueueMessage()方法加入消息队列中的,并且它在放入中就进行好排序,链表头的延迟时间小,尾部延迟时间最大
2.Looper.loop()通过MessageQueue中的next()去取消息
3.next()中如果当前链表头部消息是延迟消息,则根据延迟时间进行消息队列会阻塞,不返回给Looper message,知道时间到了,返回给message
4.如果在阻塞中有新的消息插入到链表头部则唤醒线程
5.Looper将新消息交给回调给handler中的handleMessage后,继续调用MessageQueen的next()方法,如果刚刚的延迟消息还是时间未到,则计算时间继续阻塞
Handler总结
在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(【 Looper.myLooper().quit(); 】)
Android 的消息机制主要指 Handler 的运行机制,以及 Handler 所附带的 MessageQueue 和 Looper 的工作过程,三者是一个整体。当我们要将任务切换到某个指定的线程(如 UI 线程)中执行的时候,会通过 Handler 的 send(Message message msg) 和 post(Runnable r) 进行消息的发送,post()方法最终也是通过 send() 方法来完成的。
发送的消息会插入到 MessageQueue 中(MessageQueue 虽然叫做消息队列,但是它的内部实现并不是队列,而是单链表,因为单链表在插入和删除上比较有优势),然后 Looper 通过 loop() 方法进行无限循环,判断 MessageQueue 是否有新的消息,有的话就立刻进行处理,否则就一直阻塞在那里,loop() 跳出无限循环的唯一条件是 MessageQueue 返回 null。
Looper 将处理后的消息交给 Handler 进行处理,然后 Handler 就进入了处理消息的阶段,此时便将任务切换到 Handler 所在的线程,我们的目的也就达到了。
网友评论