Handler面试知识点
- 问题1:Handler有哪些部件,这些部件的作用是什么?
- 问题2:一个线程有几个Handler?
- 问题3:一个线程有几个Looper?,如何保证?
- 问题4:Handler内存泄漏的原因?为什么其他内部类没有说过有这个问题?
- 问题5:为何主线程可以new Handler?如果想要在子线程中new Handler要做些什么准备?
- 问题6:子线程中维护的Looper,消息队列无消息的时候处理方案是什么?有什么用?
- 问题7:既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?
- 问题8:我们使用Message时应该如何创建它?
- 问题9:使用Handler的postDelay后消息队列会有什么变化?
- 问题10:Looper死循环为什么不会导致应用卡死?
- 问题11:Message、Handler、MessageQueue、Looper 的之间的关系?
- 问题12:Handler同步屏障
- 问题13:Handler怎么做到的线程间切换,子线程如何切换到主线程更新UI?
问题1:Handler有哪些部件,这些部件的作用是什么?
Handler:是处理者的意思,它用于发送和处理消息。发送消息使用 handler 的 sendMessage()
方法,post 方法等,发出的消息经过一系列地辗转处理后,会到达 Handler 的 handleMessage()
方法中。
Looper:Looper 是 MessageQueue 的管家,当调用了 Looper 的 loop()
方法后,就会进入一个无限循环中,每当发现 MessageQueue 中存在一条消息,就把它取出,并传递到 Handler 的handleMessage()
方法中。每个线程只有一个 Looper 对象
MessageQueue:一个优先级队列,接收到的Message都是即时消息那就按照先进先出原则,如果带有延迟时间的Message会根据时间来插入到队列里每个线程中只会有一个 MessageQueue 对象
Message:消息的载体
问题2:一个线程有几个Handler?
有多个,在线程中可以new多个Handler.
问题3:一个线程有几个Looper?,如何保证?
一个线程只有一个Looper,通过ThreadLocal类中的内部类ThreadLocalMap进行维持
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
ThreadLocalMap 与当前线程是一一对应的
ThreadLocalMap是ThreadLocal的静态内部类
ThreadLocalMap: <key, value>,key = this,即唯一的 ThreadLocal,value = Looper
ThreadLocal是如何保证的?如何保证唯一?》只让set一次
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); // 表示sThreadLocal 是唯一的
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) { //sThreadLocal.get()获得的值就是Looper对象
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
线程如何跟Lopper绑定的?
ThreadLocalMap是Map的存储结构,每次存的时候都会根据key(ThreadLocal)进行判断,当前的ThreadLocal是否有对应的Looper,这样就可以表示一个ThreadLocal对应唯一的一个Looper对象.
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();
}
问题4:Handler内存泄漏的原因?为什么其他内部类没有说过有这个问题?
为什么持有外部类/Activity引用?
内部类持有外部类的引用,java内部类特征:默认会持有外部的this的引用
Handler内存泄漏的原因?
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
enqueueMessage会把this(Handler)赋值给msg.target
Message–>包含Handler–>持有Activity
如果你的Message处理时间是1分钟之后、5小时之后,意味着Message会一直存在MessageQueue里面
MessageQueue–>Message–>包含Handler–>持有Activity
说明了MessageQueue会在1分钟之后、5小时之后再处理,MessageQueue等同于内存里面,当Activity调用了onDestroy(),但是由于你的Activity在MessageQueue中(内存中)持有着,那么Activity仍旧不会被释放,这个时候内存就泄漏了.
总结:Handler造成内存泄露的原因。非静态内部类,或者匿名内部类。使得Handler默认持有外部类的引用。在Activity销毁时,由于Handler可能有未执行完/正在执行的Message。导致Handler持有Activity的引用。进而导致GC无法回收Activity。
常见的内存泄漏写法
//匿名内部类
Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
//非静态内部类
protected class AppHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// TODO: 2019/4/30
}
}
}
怎么解决:
//静态内部类+弱引用
private static class MyHandler extends Handler{
private final WeakReference<MineActivity> mMineActivityWeak;
public MyHandler(MineActivity mineActivity){
mMineActivityWeak = new WeakReference<>(mineActivity);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
MineActivity mineActivity = mMineActivityWeak.get();
if(mineActivity != null){
mineActivity.number = 5;
}
}
}
Activity销毁时,清空Handler中,未执行或正在执行的Callback以及Message。
// 清空消息队列,移除对外部类的引用
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
//Handler源码中removeCallbacksAndMessages()注释含义
/**
* Remove any pending posts of callbacks and sent messages whose
* <var>obj</var> is <var>token</var>. If <var>token</var> is null,
* all callbacks and messages will be removed.
*/
public final void removeCallbacksAndMessages(Object token) {
mQueue.removeCallbacksAndMessages(this, token);
}
为什么其他内部类没有说过有这个问题?
JVM定义的,Java语法,没有为什么
内部类为什么会持有外部类的引用?
这是因为内部类虽然和外部类写在同一个文件中,但是编译后还是会生成不同的class文件,其中内部类的构造函数中会传入外部类的实例,然后就可以通过this$0访问外部类的成员。
因为在内部类中可以调用外部类的方法,变量等等,所以肯定会持有外部类的引用的。
问题5:为何主线程可以new Handler?如果想要在子线程中new Handler要做些什么准备?
为何主线程可以new Handler?
//ActivityThread.class
public static void main(String[] args) {
...
Process.setArgV0("<pre-initialized>");
// 1. 创建消息循环Looper
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
//2. 执行消息循环
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
在ActivityThread类中的main()方法中,有对主函数的Looper进行prepare()Looper.prepareMainLooper()
和loop()Looper.loop()
操作。主线程中的所有代码全部都运行在Looper.prepareMainLooper()和Looper.loop()两个函数之间。
总结:主线程已经默认初始化好了一个Looper
如果想要在子线程中new Handler要做些什么准备?,必须准备一下两个方法
Looper.prepare()
Looper.loop()
问题6:子线程中维护的Looper,消息队列无消息的时候处理方案是什么?有什么用?
消息队列无消息会一直处于等待状态.
(执行到next()方法中的nativePollOnce(ptr, nextPollTimeoutMillis);语句,处于linux等待状态。直到enqueueMessage()方法中执行了nativeWake(mPtr)语句才会唤醒nativePollOnce之后的操作。)
messageQueue消息处理机制:
① 如果消息队列不为空,当前时间还没到,会执行nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞等待,等待nextPollTimeoutMillis时间,时间到了会自己唤醒。
② 如果消息队列为空,当messageQueue执行enqueueMessage()流程的时候,会执行nativeWake(mPtr)进行唤醒。
解决方案:调用Looper的quitSafely()或quit(),
作用:1)释放内存,2)释放线程。
quitSafely()---->安全的
quit()---->不安全的
子线程调用quit()流程:
① 执行quit()流程,会将mQuitting 变量赋值为 true,并执行nativeWake(mPtr)进行线程唤醒操作;
MessageQueue.java
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true; // 进行mQuitting 设置为true
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr); // 进行唤醒
}
}
② 在MessageQueue类中的next()方法会对阻塞流程nativePollOnce(ptr, nextPollTimeoutMillis)进行唤醒。
MessageQueue.java
Message next() {
...
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis); //进行阻塞
...
}
...
if (mQuitting) { //此时mQuitting为true
dispose();
return null; //返回为null
}
...
}
③ Looper.loop流程中进行next()取消息流程
Looper.java
public static void loop() {
final Looper me = myLooper();
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) { // 往下执行loopOnce() 流程
return; //loopOnce()返回为false的时候直接return
}
}
}
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false; //此处message为null时返回false
}
...
}
当message为空时,loopOnce()返回为false的时候直接return,表现子线程退出,
问题7:既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?
MessageQueue是一个优先级队列,根据时间先后顺序排队的单链表,MessageQueue没有设置限制,可以无限加入message,但是受手机内存的限制。
在enqueueMessage()加消息过程中会进行synchronized操作,并且在next()取消息过程中也会进行synchronized操作。从而保证了线程安全性。
MessageQueue.java
往消息队列中存消息
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
... ...
}
从消息队列中取消息
MessageQueue.java
@UnsupportedAppUsage
Message next() {
... ...
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
... ...
}
为了保证安全性,加入synchronized,从而导致Handler delay的消息中的时间不完全准确。
synchronized可以修饰:方法,静态方法、代码块(object)、代码块(this)
问题8:我们使用Message时应该如何创建它?
采用obtain()
方法进行创建。采用享元设计模式,内存复用。
当MessageQueue进行释放消息的时候,其实并没有把消息释放掉,在执行recycleunchecked()方法时只是把消息里面的内容置空,然后把置空的消息放到池子中,往队列头中进行添加。
Obtain()是从池子队列头取一个点
如果采用new()
进行创建,可能会出现内存抖动
现象。
问题9:使用Handler的postDelay后消息队列会有什么变化?
如果消息队列为空,该消息不会执行,计算等待时间,让消息进行wait。
问题10:Looper死循环为什么不会导致应用卡死?
每个app都有一个自己的虚拟机。
App启动方式:Launcher—>application—>zygote—>虚拟机—>ActivityThread
所有的生命周期都发运行在Loop里面,都是以消息的方式存在
主线程的唤醒方式有哪些?
① 输入的事件
② Looper添加消息
ANR产生原因:
①在5秒内没有响应输入的事件(例如:按键按下,屏幕触摸)
②BroadcastReceiver在10秒内没有执行完毕
每一个事件就相对于一个message。
为什么不会卡死:
疑惑点:Message msg = queue.next();
此时主线程处于休眠状态,卡死是ANR,休眠和ANR是两码事互不干扰
问题11:Message、Handler、MessageQueue、Looper 的之间的关系?
首先,是这个 MessageQueue
,MessageQueue是一个消息队列,它的数据结构形式是有单链表
实现的优先级队列
,它可以存储 Handler 发送过来的消息,其内部提供了进队和出队的方法来管理这个消息队列,其出队和进队的原理是采用单链表的数据结构进行插入和删除的,即enqueueMessage()方法和 next()方法。这里提到的 Message,其实就是一个Bean 对象,里面的属性用来记录 Message 的各种信息。
然后,Looper
是一个循环器,它可以循环的取出MessageQueue 中的 Message,其内部提供了 Looper 的初始化和循环出去Message 的方法,即 prepare()方法和loop()方法。在 prepare()方法中,Looper会关联一个MessageQueue,而且将 Looper 存进一个 ThreadLocal 中,在loop()方法中,通过 ThreadLocal 取出 Looper,使用MessageQueue的next()方法取出 Message 后,判断 Message 是否为空,如果是则 Looper 阻塞,如果不是,则通过 dispatchMessage()方法分发该 Message 到 Handler 中,而Handler 执行 handlerMessage()方法,由于 handlerMessage()方法是个空方法,这也是为什么需要在 Handler 中重写 handlerMessage()方法的原因。这里要注意的是Looper 只能在一个线程中只能存在一个。这里提到的ThreadLocal,其实就是一个对象,用来在不同线程中存放对应线程的 Looper。
最后,Handler
是Looper 和MessageQueue的桥梁,Handler内部提供了发送 Message 的一系列方法,最终会通过 MessageQueue的enqueueMessage()方法将 Message 存进 MessageQueue 中。我们平时可以直接在主线程中使用 Handler,那是因为在应用程序启动时,在入口的 main 方法中已经默认为我们创建好了 Looper。
问题12:Handler同步屏障
@TestApi
public int postSyncBarrier() {
// 这里传入的时间是从开机到现在的时间戳
return postSyncBarrier(SystemClock.uptimeMillis());
}
/**
* 这就是创建的同步屏障的方法
* 同步屏障就是一个同步消息,只不过这个消息的target为null
*/
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
// 从消息池中获取Message
final Message msg = Message.obtain();
msg.markInUse();
// 初始化Message对象的时候,并没有给Message.target赋值,
// 因此Message.target==null
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
// 这里的when是要加入的Message的时间
// 这里遍历是找到Message要加入的位置
while (p != null && p.when <= when) {
// 如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步
// 消息里有时间小于T,则prev也不为null
prev = p;
p = p.next;
}
}
// 根据prev是否为null,将msg按照时间顺序插入到消息队列的合适位置
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
在这里可以看到,Message对象初始化的时候并没有给target赋值,因此target== null的来源就找得到了。这样就可以插入一条target== null的消息,这个消息就是一个同步屏障。
总结:同步屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,因此可以认为,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。
问题13:Handler怎么做到的线程间切换,子线程如何切换到主线程更新UI?
Handler怎么做到的线程间切换?
在创建Handler的时候需要传入目标线程的Looper。(没有传入Looper默认拿当前线程的Looper,如果当前线程也没有准备好Looper会抛异常)而当sendMessage的时候,会将当前的Handler对象赋值给Message中的target变量,并将该Message存到传入目标线程Looper的MessageQueue中。
当Looper消费Message的时候便会拿到Message中的target执行dispatchMessage()
方法,从而实现线程切换。
子线程如何切换到主线程更新UI?
在主线程创建的Handler默认会传入主线程的Looper,Looper和线程是相互绑定的,意味着主线程的Looper对应的线程就是主线程,所以在子线程中发送Message,Message.target 绑定的是主线程的Handler,最终会回到主线程Handler的HandleMessage里去处理。