Android的Handler的常见面试问题总结
Posted 小图包
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android的Handler的常见面试问题总结相关的知识,希望对你有一定的参考价值。
本文是对常见面试问题的分析,关于Handler的运行机制详细分析见:android Handler的源码分析
常见问题包括:
1、Handler是怎么实现切换线程的?
2、MessageQueue是怎么增删消息的
3、一个线程可以有几个Handler?几个Looper?几个MessageQueue?
4、Looper取出消息又如何发送到指定的Handler呢?
5、简述ThreadLoacal的原理?
6、如何处理Handler使用不当造成的内存泄漏?
7、在UI中创建的Handler,通过post方式发送的消息在run方法中可以进行UI更新吗?
8、Handler机制——同步屏障 是什么,哪里用过同步屏障?
9、Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?
10、主线程的Looper何时退出?能否手动退出?
11、系统为什么不建议在子线程中访问UI?
12 、Android IdleHander 是否了解,是否使用过。
13、如何自定义一个Hander
一、Handler是怎么实现切换线程的?
1 调用prepare方法
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对象保存在了一个ThreadLocal对象中.
接着看创建了一个Loper对象,在Loper做了什么
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
// 创建了一个MessageQueue对象,这个就是一个消息队列用来保存我们发送的Message,其实这个MessageQUueue是一个单链表。
创建了一个MessageQueue对象
2 创建Handler
public Handler(Callback callback, boolean async) {
......
mLooper = Looper.myLooper();
......
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Hanlder中获取了一个Looper对象,这个Looper中保存了一个mQueue,
3 调用loop()
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {// 无限循环
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
......
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {// 处理消息
msg.target.dispatchMessage(msg);
......
}
这里看到 loop()方法中获取了Looper对象,取出了消息队列mQueue,然后开始循环,同时调用了MessageQueue的next()方法取出一个消息最后调用了这个消息的dispathchMessage();这里的msg.tagget.dispatchMessage()就是Handler中的消息处理的方法
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
通过上面代码我们知道调用了Hanlder的handleMessage(msg)
4 发送消息
调用sendMessage()等方法发送消息,追踪调用了sendMessageAtTime()的方法,然后调用了enqueueMessage()方法,最后在queue.enqueueMessage(msg, uptimeMillis);在queue中处理了消
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {// 同步的操作,同一时间只能一个线程操作
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
......
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {// 判断消息要执行的时间顺序
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;// 把消息插队链表
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
}
return true;
}
通过消息要执行的时间,最后把消息插入链表中。主线程的loop()方法一直在循环处理着消息,发送的消息就会到loop()方法中去,最后交给Handler处理。
线程的切换
发送的过程,我们在不同的线程发送消息,线程之间的资源是共享的。也就是任何变量在任何线程都可以修改,只要做并发操作就好了。上述代码中插入队列就是加锁的synchronized,Handler中我们使用的是同一个MessageQueue对象,同一时间只能一个线程对消息进行入队操作。消息存储到队列中后,主线程的Looper还在一直循环loop()处理。这样主线程就能拿到子线程存储的Message对象,这样就完成了线程的切换.
二 MessageQueue是怎么增删消息的?
增加:通过enqueueMessage方法,通过对消息添加时间和之前消息的判断,将新添加的消息放置在栈顶
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));
}
删除:通过next方法取出消息,在loop()中进行消息的分发处理,在next返回消息前对消息属性进行修改
// MessageQueue中的next()方法
msg.next = null;
msg.markInUse();
// Message中的markInUse()方法,表明消息已经使用
void markInUse() {
flags |= FLAG_IN_USE;
}
三、一个线程可以有几个Handler?几个Looper?几个MessageQueue?
一个线程能够创建多个Handler,Handler跟Looper没有对应关系,线程才跟Looper有对应关系,一个线程对应着一个Looper,如下所示:
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
//将Looper存到当前线程的ThreadLocal中
sThreadLocal.set(new Looper(quitAllowed));
}
创建Handler时,需要先调用Looper的prepare方法,在该方法中,会首先判断sThreadLocal.get()是否为空,如果不为空就抛出异常,内容是:一个线程只能创建一个Looper。如果sThreadLocal.get()为空,则会创建一个Looper对象并存入sThreadLocal中。
在调用Looper.prepare()时,会判断该线程的Looper是否为空,只有为空的情况才会调用Looper的构造方法,创建MessageQueue,因此只有一个。
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
四、 Looper取出消息又如何发送到指定的Handler呢?
从handler发送消息,最终会调用Handler类中的enqueueMessage方法,该方法源码如下所示:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
1 msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
1 处Message类中的target是什么,该target是一个Handler类型,所以msg的target指向了当前Handler对象,然后msg被加入到消息队列,通过调用下面代码
queue.enqueueMessage(msg, uptimeMillis);
一个线程中包含多个Handelr对象,而在消息的添加分发时,通过Message的target(Handler)标注。
五、简述ThreadLoacal的原理?
通过set和get方法,其内部维护这一个Map集合,其中key是当前的线程,value为需要存储的值。
六 、 如何处理Handler使用不当造成的内存泄漏?
- 有延时消息,在界面关闭后及时移除Message/Runnable,调用
handler.removeCallbacksAndMessages(null)
- 内部类导致的内存泄漏改为静态内部类,并对上下文或者Activity/Fragment使用弱引用。
七 、在UI中创建的Handler,通过post方式发送的消息在run方法中可以进行UI更新吗?
通过查看源码可以发现,其最终调用的还是sendMessage相关的方法,但是如果传入的Runnable形式,其内部会对其进行了封住成Message类。
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
在消息处理结束后,其回调至Runnable 的run方法中,需要注意的是这里的仍然是在UI线程中,因为我们创建的Handler是在UI线程中,且Handler将Runnable内部封住成Message的形式,在消息分发时首先检测callback是否为空,如下所示:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
...
}
}
如果不为空则调用handleCallback方法,但是该方法实际上是调用message.callback.run(),而message.callbacK是我们在添加消息时赋值的Runable,所以最终调用的是Runable中的run()方法,因此还是回到UI线程中,可以更新UI界面。
private static void handleCallback(Message message) {
message.callback.run();
}
八 Handler机制——同步屏障 是什么,哪里用过同步屏障?
其中普通消息又称为同步消息,屏障消息又称为同步屏障。
我们通常使用的都是普通消息,而屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,因此可以这样认为:屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。
哪里使用过同步屏障 在我之前的文章讲到过,链接如下
九、Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?
Looper会在线程中不断的检索消息,如果是子线程的Looper死循环,一旦任务完成,用户应该手动退出,而不是让其一直休眠等待。(引用自Gityuan)线程其实就是一段可执行的代码,当可执行的代码执行完成后,线程的生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder 线程也是采用死循环的方法,通过循环方式不同与 Binder 驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。Android是基于消息处理机制的,用户的行为都在这个Looper循环中,我们在休眠时点击屏幕,便唤醒主线程继续进行工作。
主线程的死循环一直运行是不是特别消耗 CPU 资源呢? 其实不然,这里就涉及到 Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
十、 主线程的Looper何时退出?能否手动退出?
在App退出时,ActivityThread中的mH(Handler)收到消息后,执行退出。
/ActivityThread.java
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;
如果你尝试手动退出主线程Looper,便会抛出如下异常
Caused by: java.lang.IllegalStateException: Main thread not allowed to quit.
at android.os.MessageQueue.quit(MessageQueue.java:428)
at android.os.Looper.quit(Looper.java:354)
at com.jackie.testdialog.Test2Activity.onCreate(Test2Activity.java:29)
at android.app.Activity.performCreate(Activity.java:7802)
at android.app.Activity.performCreate(Activity.java:7791)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
为什么不允许退出呢,因为主线程不允许退出,一旦退出就意味着程序挂了,退出也不应该用这种方式退出。
十一 、系统为什么不建议在子线程中访问UI?
这是因为 Android 的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
- 首先加上锁机制会让UI访问的逻辑变得复杂
- 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
所以最简单且高效的方法就是采用单线程模型来处理UI操作。(安卓开发艺术探索)
十二 、Android IdleHander 是否了解,是否使用过。
IdleHandler是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调。
public static interface IdleHandler {
boolean queueIdle();
}
该方法返回一个boolean值,
如果为false则执行完毕之后移除这条消息(一次性完事),
如果为true则保留,等到下次空闲时会再次执行(重复执行)。
看MessageQueue的源码可以发现有两处关于IdleHandler的声明,分明是:
1 存放IdleHandler的ArrayList(mIdleHandlers),
2 还有一个IdleHandler数组(mPendingIdleHandlers)。
后面的数组,它里面放的IdleHandler实例都是临时的,也就是每次使用完(调用了queueIdle方法)之后,都会置空(mPendingIdleHandlers[i] = null)
下面看下MessageQueue的next()方法可以发现确实是这样
Message next() {
......
for (;;) {
......
synchronized (this) {
// 此处为正常消息队列的处理
......
if (mQuitting) {
dispose();
return null;
}
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
pendingIdleHandlerCount = 0;
nextPollTimeoutMillis = 0;
}
}
就在MessageQueue的next方法里面。
流程是这样的:
- 如果本次循环拿到的Message为空,或者!这个Message是一个延时的消息而且还没到指定的触发时间,那么,就认定当前的队列为空闲状态,
- 接着就会遍历mPendingIdleHandlers数组(这个数组里面的元素每次都会到mIdleHandlers中去拿)来调用每一个IdleHandler实例的queueIdle方法,
- 如果这个方法返回false的话,那么这个实例就会从mIdleHandlers中移除,也就是当下次队列空闲的时候,不会继续回调它的queueIdle方法了。
处理完IdleHandler后会将nextPollTimeoutMillis设置为0,也就是不阻塞消息队列,当然要注意这里执行的代码同样不能太耗时,因为它是同步执行的,如果太耗时肯定会影响后面的message执行。
其他可以使用的地方比如:
1.Activity启动优化:onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间
2.想要在一个View绘制完成之后添加其他依赖于这个View的View,当然这个用View#post()也能实现,区别就是前者会在消息队列空闲时执行
3一些第三方库中有使用,比如LeakCanary,Glide中有使用到。
至此Handler面试常问的分析完的了。
以上是关于Android的Handler的常见面试问题总结的主要内容,如果未能解决你的问题,请参考以下文章