Android面试整理 - Handler
Handler的实现原理
答案参考自:


子线程中能不能直接new一个Handler,为什么主线程可以?主线程的Looper第一次调用loop方法,什么时候,哪个类?
答案参考自:
可以在子线程直接new一个Handler,不过需要在子线程里先调用Looper#prepare。new一个Handler后,还需要调用Looper#loop方法。
1 | |
有人会问,在主线程中为什么没看到Looper.prepare()?其实系统已经给我们调用了,不过调用的是Looper.prepareMainLooper(),如下源码所示:
1 | |
main方法是整个android应用的入口,在子线程中调用Looper.prepare()是为了创建一个Looper对象,并将该对象存储在当前线程的ThreadLocal中。
每个线程都会有一个ThreadLocal,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,并且这种变量只在本线程的生命周期内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度。Looper.loop()方法是为了取出消息队列中的消息并将消息发送给指定的handler,通过msg.target.dispatchMassage()方法。
Handler导致的内存泄露原因及其解决方案
当不再需要某个实例后,这个对象却仍然被引用,阻止被垃圾回收(Prevent from being bargage collected),这个情况就叫做内存泄露(Memory Leak)。
考虑以下的代码;
1 | |
虽然不明显,但是这段代码可能导致内存泄露。Android Lint会提示以下信息:
1 | |
它到底是如何泄露的呢?
当一个
Android应用程序启动的时候,Android框架为这个程序的主线程即UI线程创建了一个Looper对象,用于处理Handler中的Message。
Looper实现了一个简单的消息队列MessageQueue,不断循环的处理其中的message。
所有的应用程序框架的事件(比如Activity生命周期的调用,按钮的点击等)都被封装在这个Message对象里,然后被加入到Looper的MessageQueue,最后一个一个的处理这些Message。
注意,Looper在整个应用程序的生命周期中一直存在。在主线程中实例化一个
Handler对象的时候,就和它关联了主线程Looper的消息队列MessageQueue。
被发送到这个消息队列的Message将保持对这个Handler对象的引用,这样框架就可以在处理这个Message的时候调用Handler.handleMessage(Message)来处理消息了。
(也就是说,只要没有处理到这个Message,Handler就一直在队列中被引用)。在
Java中,非静态内部类和匿名内部类都隐式的保持了一个对外部类outerclass的引用。但是静态内部类不会有这个引用。
正确的解决方法:
Handler静态内部类 +WeakReference<Activity>
1 | |
- 静态
Runnable,避免对外部类的引用
1 | |
静态内部类和非静态内部类的区别很微小,但是开发人员必须了解。
那么底线是什么?
当内部类可以独立于Activity的生命周期而存在的时候,应该避免使用非静态内部类,应该用静态内部类并且使用WeakReference保持对Activity的引用。
深入理解
像下面这样使用handler的时候,其实是将handler定义为了匿名内部类:
1 | |
而匿名内部类会默认持有外部类(MainActivity)的引用。
学过handler的都知道,handler发送消息后,消息会进行入队操作,在enqueueMessage方法中:
1 | |
this指的就是handler,所以handler被message持有了,而message放入消息队列后,message又被MessageQueue持有了,而MessageQueue是在创建Looper的时候生成的:
1 | |
所以MessageQueue又被looper所持有。如果这个handler是主线程的handler,那么此时的looper就是指的主线程的Looper,它的声明如下:
1 | |
可以看到主线程的looper是static静态变量,而static静态变量在垃圾回收的时候是会被当做GC Root的,静态变量的生命周期与APP的生命周期、与虚拟机的生命周期是一样的,所以正是因为这个持有链的存在,导致了内存泄露。
引用链大致如下:
(static) Looper -->|持有| MessageQueue -->|持有| Message -->|持有| Handler -->|持有| MainActivity
所以解决 handler 内存泄露的办法就是要破坏这个持有链,比如只要 handler 不被 activity 持有就可以,所以可以把 handler 定义为static,因为 static 不会持有外部类,这样 handler 就不会持有 activity 了。
怎样判断一个内部类有没有被持有外部类?
比如上面的handler定义,没有加static的时候,在handleMessage方法里面可以正常使用MainActivity.this,这说明它持有了外部类。而一旦Handler加上static关键字,在handleMessage方法内部就不能再使用MainActivity.this,说明它没有持有外部类。
(为什么static变量,不会造成内存泄露?static 不会去持有外部类)
MessageQueue是什么数据结构
MessageQueue 是一个基于时间排序的优先队列。
Message对象创建的方式有哪些 & 区别?
创建Message对象的时候,有三种方式,分别为:
Message msg = new Message();Message msg = Message.obtain();Message msg = handler.obtainMessage();
分析
Message msg = new Message();这种就是直接初始化一个Message对象,没有什么特别的。
Message msg = Message.obtain();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}从注释可以得知,从整个
Message池中返回一个新的Message实例,通过obtainMessage能避免重复Message创建对象。Message msg = handler1.obtainMessage();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public final Message obtainMessage() {
return Message.obtain(this);
}
public static Message obtain(Handler h) {
Message m = obtain();
m.target = h;
return m;
}
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}可以看到,第二种跟第三种其实是一样的,都可以避免重复创建Message对象,所以建议用第二种或者第三种任何一个创建Message对象。
Message.obtain()怎么维护消息池的?
使用了享元设计模式,当前message执行完后,把message置为空,然后重新给message进行赋值。 通过链表的形式,进行了复用和回收
Handler 有哪些发送消息的方法
1 | |
其中,版本3 使用到的 sendToTarget 方法只适用于有target值的Message。
1 | |
Handler的post与sendMessage的区别和应用场景
答案参考自:

子线程能不能更新UI
答案参考自:
极端情况下是可以的。
更新UI后会立即通过
ViewRootImpl类执行里面的performTraversal方法。在
performTraversal方法前,还会先执行一个checkThread方法。如果监测到当前的线程不是主线程,就会抛出异常。1
2
3
4
5
6void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}通过源码可以发现,
ViewRootImpl类的创建是在回调了onResume方法之后。所以我们在onCreate方法中通过子线程立即更新UI时,由于该类并没有创建,所以无法检测当前线程是否为主线程,所以程序没有崩溃一样能跑起来,如果修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经创建了,可以执行checkThread方法检查当前线程。
为什么Android系统不建议子线程访问UI
Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以Android中规定只能在UI线程中访问UI。
postDelay后消息队列有什么变化,假设先 postDelay 10s, 再postDelay 1s, 怎么处理这2条消息
答案参考自:
如果队列中只有这个消息,那么消息不会被发送,而是计算到时唤醒的时间,先将Looper阻塞,到时间就唤醒它。
但如果此时要加入新消息,该消息队列的对头跟delay时间相比更长,则插入到头部,按照触发时间进行排序,队头的时间最小、队尾的时间最大。(消息队列为优先队列)
MessageQueue的enqueueMessage()方法如何进行线程同步的
通过源码可以发现,enqueueMessage 方法中,通过了 synchronized 关键字对 MessageQueue 进行了上锁的处理。保证了线程的同步。
ThreadLocal在Handler机制中的作用
ThreadLocal 更多细节:
- [ThreadLocal 解析 转载] | 洛语 の Blog (luoyu-ying.github.io)
Threadlocal内部是一个Map实现,以当前线程Threadlocal为键,以Looper为值进行绑定,**保证一个线程对应一个Looper**。
当Activity有多个Handler的时候,怎么样区分当前消息由哪个Handler处理
在Looper#loop中,Looper把message直接交给了target即发送这个消息的handler处理。
Handler 如何与 Looper 关联的
通过 ThreadLocal 进行关联。
Looper 如何与 Thread 关联的
通过 ThreadLocal 进行关联。
通过Handler如何实现线程的切换
实际线程间切换,就是通过线程间共享变量实现的。
在A线程new handler(),在b线程调用这个handler发送消息,这个message发送到了,A线程中的 messageQueue里面,又回到了a线程中执行。
Android中为什么主线程不会因为Looper#loop里的死循环卡死?
主线程确实是阻塞的,不阻塞那APP怎么能一直运行?
所以说主线程阻塞是一个伪命题,只不过是没有弄明白既然阻塞了,为什么还能调用各种声明周期而已。
调用生命周期是因为有Looper,有MessageQueue,还有沟通的桥梁Handler,通过IPC机制调用Handler发送各种消息,保存到MessageQueue中,然后在主线程中的Looper提取了消息,并在主线程中调用Handler的方法去处理消息.最终完成各种声明周期。
MessageQueue#next 在没有消息的时候会阻塞,如何恢复?
用 MessageQueue#enqueueMessage 时会唤醒 MessageQueue,这个方法会被 Handler#sendMessage、Handler#post 等一系列发送消息的方法调用。