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
等一系列发送消息的方法调用。