handler作为高级安卓面试必问问题之一,其重要性不言而喻。 它对上层应用开发的影响无处不在, 例如 handler内存泄漏、线程通信、消息循环模式、数据库操作应该放在哪个线程、handler.handleMessage能否执行耗时任务、屏幕触摸事件分发机制、Animator动画机制、Activity启动流程… …跟handler有关的东西无处不在,但凡是安卓java层的东西几乎都能跟handler扯上一点关系。
全面的理解Handler需要涉及到android系统的许多方面,如:安卓主线程消息轮询机制、linux线程通信、队列数据结构(入队\出队\查找)、享元模式、多线程数据安全、AMS… …
或许每个安卓开发都能对handler说上一两个甚至三四个知识点。我也曾经自认为很仔细的看过Handler源码,但是过一两个月又全都忘记。写这篇帮助来自己巩固对handler的全面理解,以及做个笔记,下次需要相关知识时来看一眼,能很快理清每一个细节,以便在不同的业余场景下能合理运用相关技术点做出最优解,顺便帮助其它读者(PS:做某件事之前最好树立一个比较崇高的立意,这样能给自己更强大的动图,也不至于轻敌)
针对上面的问题,如果读者你能很快给出准确答案,并指出相关类的相关方法,那恭喜你,你已经出神入化了,不需要再看我这篇文章。 如果你的感觉是模棱两可,那跟随本博主一起遍历一下handler的每一个细节吧。
handler翻译过来是“处理者”,那自然是用来处理某些事物的,准确说是用于处理需要在多线程间相互通信的事物。例如主线程需要处理某件事,但何时处理则需要等待其它线程来通知。
下面以一个简化化版的 Handler为例,只列举核心方法,说明handler类的结构:
##简化版的 Handler、只提取核心变量和方法
public classs Handler{
private Looper looper;
private MessageQueue queue;
public Handler(){
//获取当前线程的 looper对象
looper = Looper.mylooper()
//获取此Looper对象的消息队伍
queue = looper.mQueue;
}
public void handleMessage(@NonNull Message msg){
重写此方法,用于处理事件
}
public void sendXXXMessage(){
各种发送消息的方法,最终都会调用 enqueueMessage 方法
}
private void enqueueMessage(MessageQueue queue, Message msg,long uptimeMillis) {
将消息放入到消息队列,若消息队列读端因无消息而阻塞,则会唤醒读端的线程
}
public void dispatchMessage(@NonNull Message msg) {
分发事件:将事件交给对应的回调(回调可能是msg.callback、handler.mCallback、handler.handleMessage)
}
public static Message obtain() {
通过Message的静态变量 sPoolSync 缓存池头指针,以享元模式获取可利用的Message对象,避免内存抖动
}
}
单纯看handler类的方法,它可圈可点的地方并不多,它的一些重要的功能点都是由Looper和MessageQueue去完成。
它的各种sendXXXMessage方法,其内部最终都是调用enqueueMessage方法,将新的消息插入到MessageQueue的对应位置,对应的规则是按消息所需要执行的时间点来排序的有序消息队列。
发送延迟消息,延迟指定的毫秒数后才来执行这个消息。
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
消费此消息的方式
##源码分析
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
##简化版的 MessageQueue.java, 只提取核心变量和方法
public final class MessageQueue {
//native层通信的信号量
private long mPtr;
//消息队列头指针
Message mMessages;
//是否允许退出此消息循环(目前只有主线程的消息循环会用到)
private final boolean mQuitAllowed;
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
将新消息按执行时间点的排序插入到 消息队列中
根据消息的执行时间戳,决定是否唤醒读端
}
}
Message next() {
//取出队头的消息,若队列为空,则会阻塞住,直接有新消息时被唤醒
死循环{
nativePollOnce(xx)
synchronized (this) {
判断队头消息是否达到执行时间戳,若达到则队头出队,否则继续阻塞,指定时长后唤醒
}
}
}
//native方法,唤醒读端
private native static void nativeWake(long ptr);
//native方法,读端判断是否有消息可读,若没有则阻塞信
private native void nativePollOnce(long ptr, int timeoutMillis)
//退出消息循环
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
}
}
下面还是以简化版的Looper.java源码为例,来说明Looper如何实现“循环做某件事情”的:
简化版的 Looper.java, 只提取核心变量和方法
public final class Looper {
//用于保存各个线程独有的Looper,所以是静态变量
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
//当前Looper实例维护的消息队列
final MessageQueue mQueue;
//静态方法,为执行此方法的线程创建一个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));
}
//创建主线程的Looper,只能由操作系统调用,应用层不需要调用,调了也会异常
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
public static void loop() {
Looper me = 取出调用此方法的线程的独有Looper
//取出消息队列
MessageQueue queue = me.mQueue;
for(;;){
//尝试读取队头的消息,若队列为空,则阻塞。直至写端写入新消息,或延时消息到达执行时间就会被唤醒。通信机制由native负责完成。
Message msg = queue.next();
//分发出队的消息
msg.target.dispatchMessage(msg);
}
}
}
下面还是以简化版的ThreadLocal.java源码为例,来说明ThreadLocal如何帮助Looper解决线程独有且安全的问题,先给出结论:
##简化版的 ThreadLocal, 只提取核心变量和方法
public class ThreadLocal<T> {
//给调用此方法的线程保存一个“此线程独有的变量”
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//返回 调用此方法的线程 所独有的变量
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//线程独有的Map,内部结构类似 HashMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private void set(ThreadLocal<?> key, Object value){
}
private Entry getEntry(ThreadLocal<?> key) {
}
}
}
一个线程有几个 Handler实例? 有几个Looper? 有几个MessageQueue?
一个线程只能创建一个Looper实例,对应一个MessageQueue,而handler可以随意创建,只要在创建handler之前调用过 Looper.prepare的线程都可以创建handler任意个。
Handler内存泄漏的原因? 为什么其它的内部类(如viewHolder)不会内存泄漏?
当handler发送延迟消息时,此时activity又关闭了就会短暂泄漏,因为handler被msg.target引用,msg被MessqgeQueue引用,messageQueue被Looper引用,Looper被主线程的thread.ThreadLocalMap引用,而主线程肯定是常驻内存的,只要主线程不销毁,那么handler永远是在引用链上的,而handler又作为匿名内部类隐式持有 Activity/Fragment 导致 activity内存泄漏。
为什么主线程可以随意new Handler? 子线程需要怎样才能 new Handler?
任何线程只要调用了Looper.prepare()后就可以new Handler,主线程的Looper.prepare是 ActivityThread.main方法是由系统环境调用的。 子线程调用完Looper.prepare后也可以任意new Handler()。
子线程中维护的Looper,当它的消息队列再无消息时如何妥善处理?
当子线程Looper.prepare时入参传的是允许退出,那么可以调用Looper.myLooper().quit() 来退出子线程的消息循环
主线程的handler,其它所有线程都可以使用这个handler发消息,它是如何确保线程安全的?
是的,所有线程都可以调用。其内部是在MessageQueue.enqueueMessage()方法内通过 synchronized(this)来保证多线程安全的。
发消息时如何创建Message最合适?new Message吗?
handler.obtain() 或者 Message.obtain() 利用 共享元素模式(享元模式) 来复用 Message对象,避免快速创建后双回收,引起的内存抖动,抖动多了就会有大量内存碎片,当需要创建大对象时就有容易OOM。
使用handler.postDelay后消息队列有什么变化?
往要对应消息队列中按 执行时间戳顺序 插入到队列中,当然这样称呼为队列不太准确,严格意义上的队列只能是队尾入队,而这里却在队中间插入元素。 虽然不准确,但数据结构应该活学活用,以满足业务要求,而不是为了学术概念而限制自己。
Looper.loop的死循环为什么不会导致应用卡死?
死循环这个词描述的不太准确,死循环用来描述日常生活中的现象这没问题,但用来描述loop那就太以偏概全了, 安卓主线程本来就是 阻塞/唤醒模式的消息循环机制,并不是死循环,无消息时就会阻塞以便释放cpu资源,有消息时才会处理。
另外,应用层的几乎绝大部分类都跟此消息机制有关,例如application生命周期方法、四大组件生命周期方法、屏幕触摸事件、屏幕刷新60、90、120hz都是通过 handler发消息来通知 应用层。 这如果能卡死,这些功能都没有,那你的app启动后就是白板一块。
而且,iOS、windows开发,但凡是有界面的应用程序,都有类似的消息循环机制,它这样设计的根本目的就是要保证用户交互不能被阻塞。
postDelay指定时长的消息,为何在指定时长后会得到执行?内部如何实时定时器?
定时器是通过native方法 nativePollOnce()来实现的,它的第二参数可以指定一个延时参数,当参数大于0时,此方法调用完后会进入阻塞状态,达到指定延时时长后才会自动唤醒。
系统所提供postDelay的时长绝对准确吗?
不是绝对准确,有可能受前面的消息执行耗时的影响,例于延时1秒的消息,之前还有一条消息执行耗时2秒,那必须等到这2秒消息执行完,才会再来执行这个延时1秒的消息。
Message与Messenger的是什么关系?
Message是消息体。 Messenger是用于进程间通信的上层包装类,它和Handler结合起来,可以让进程间通信,看起来像handler线程间通信那样简单。当然Messenger内部通信机制肯定都是binder,但是手写binder极其麻烦,即使用aidl也远没有Messenger方便。
MessageQueue的IO复用原理?
(待验证)android_os_MessageQueue_nativePollOnce ,因为这里的 IO 机制采用 epool ,当它没有消息时会调用 wait() 函数释放 CPU 进入休眠等待,当有消息来临会通过管道写入来通知唤醒。
Linux的IO多路复用模型还有 poll 和 epoll ,这里说一下它们之间的区别,poll 可监视的 IO数量大于 select,而 epoll 和其他两个函数的区别就是不会轮询文件描述符来操作 IO,当一个IO完毕就直接通知刷新,而不是一直轮询判断可读写的状态来刷新,简单的说,epoll 只会刷新已经成功的 IO,而其他两个函数判断 IO 是否已成功是用轮询的方式,细心的朋友会发现,我们的这个 IO 多路复用好像也没有比阻塞或非阻塞 IO 模型强到哪去,而且还要往函数里添加 socket 监听回调,IO 多路复用的核心就在于同一时刻一个逻辑流也就是一个线程可以监听操作多个 IO,而其他 IO 模型只能通过多线程来进行多个 IO 的需求。