Android Handler面试总结

Android面试中,有关Handler的面试是一个离不开的话题,下面我们就有关Handler的面试进行一个总结。

1,Handler、Looper、MessageQueue、线程的关系

  • 一个线程只会有一个Looper对象,所以线程和Looper是一一对应的。
  • MessageQueue对象是在new Looper的时候创建的,所以Looper和MessageQueue是一一对应的。
  • Handler的作用只是将消息加到MessageQueue中,并后续取出消息后,根据消息的target字段分发给当初的那个handler,所以Handler对于Looper是可以多对一的,也就是多个Hanlder对象都可以用同一个线程、同一个Looper、同一个MessageQueue。

综上,Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的。

2,主线程为什么不用初始化Looper

因为应用在启动的过程中就已经初始化了一个主线程Looper。每个java应用程序都是有一个main方法入口,Android是基于Java的程序也不例外,Android程序的入口在ActivityThread的main方法中,代码如下:

// 初始化主线程Looper

Looper.prepareMainLooper();

...

// 新建一个ActivityThread对象

ActivityThread thread = new ActivityThread();

thread.attach(false, startSeq);

// 获取ActivityThread的Handler,也是他的内部类H

if (sMainThreadHandler == null) {

sMainThreadHandler = thread.getHandler();

}

...

Looper.loop();

// 如果loop方法结束则抛出异常,程序结束

throw new RuntimeException("Main thread loop unexpectedly exited");

}

可以看到,main方法中会先初始化主线程Looper,新建ActivityThread对象,然后再启动Looper,这样主线程的Looper在程序启动的时候就跑起来了。并且,我们通常认为 ActivityThread 就是主线程,事实上它并不是一个线程,而是主线程操作的管理者。

3,为什么主线程的Looper是一个死循环,但是却不会ANR

因为当Looper处理完所有消息的时候会进入阻塞状态,当有新的Message进来的时候会打破阻塞继续执行。

首先,我们看一下什么是ANR,ANR,全名Application Not Responding。当我发送一个绘制UI 的消息到主线程Handler之后,经过一定的时间没有被执行,则抛出ANR异常。下面再来回答一下,主线程的Looper为什么是一个死循环,却不会ANR?Looper的死循环,是循环执行各种事务,包括UI绘制事务。Looper死循环说明线程没有死亡,如果Looper停止循环,线程则结束退出了,Looper的死循环本身就是保证UI绘制任务可以被执行的原因之一。

关于这个问题,我们还可以得到如下的一些结论:

  • 真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身。
  • 在主线程以外,会有其他的线程来处理接受其他进程的事件,比如Binder线程(ApplicationThread),会接受AMS发送来的事件
  • 在收到跨进程消息后,会交给主线程的Hanlder再进行消息分发。所以Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到onCreate方法。
  • 当没有消息的时候,会阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,所以死循环也不会特别消耗CPU资源。

4,Message是怎么找到它所属的Handler然后进行分发的

在loop方法中,找到要处理的Message需要调用下面的一段代码来处理消息:

msg.target.dispatchMessage(msg);

所以是将消息交给了msg.target来处理,那么这个target是什么呢,通常查看target的源头可以发现:

private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {

msg.target = this;

return queue.enqueueMessage(msg, uptimeMillis);

}

在使用Hanlder发送消息的时候,会设置msg.target = this,所以target就是当初把消息加到消息队列的那个Handler。

5,Handler是如何切换线程的

使用不同线程的Looper处理消息。我们知道,代码的执行线程,并不是代码本身决定,而是执行这段代码的逻辑是在哪个线程,或者说是哪个线程的逻辑调用的。每个Looper都运行在对应的线程,所以不同的Looper调用的dispatchMessage方法就运行在其所在的线程了。

6,post(Runnable) 与 sendMessage 有什么区别

我们知道,Hanlder中发送消息可以分为两种:post(Runnable)和sendMessage。首先,我们来看一下源码:

public final boolean post(@NonNull Runnable r) {

return sendMessageDelayed(getPostMessage(r), 0);

}

private static Message getPostMessage(Runnable r) {

Message m = Message.obtain();

m.callback = r;

return m;

}

public final boolean sendMessage(@NonNull Message msg) {

return sendMessageDelayed(msg, 0);

}

可以看到,post和sendMessage的区别就在于,post方法给Message设置了一个callback回调。那么,那么这个callback有什么用呢?我们再转到消息处理的方法dispatchMessage中看:

public void dispatchMessage(@NonNull Message msg) {

if (msg.callback != null) {

handleCallback(msg);

} else {

if (mCallback != null) {

if (mCallback.handleMessage(msg)) {

return;

}

}

handleMessage(msg);

}

}

private static void handleCallback(Message message) {

message.callback.run();

}

可以看到,如果msg.callback不为空,也就是通过post方法发送消息的时候,会把消息交给这个msg.callback进行处理;如果msg.callback为空,也就是通过sendMessage发送消息的时候,会判断Handler当前的mCallback是否为空,如果不为空就交给Handler.Callback.handleMessage处理。

所以post(Runnable) 与 sendMessage的区别就在于后续消息的处理方式,是交给msg.callback还是 Handler.Callback或者Handler.handleMessage。

7,Handler如何保证MessageQueue并发访问安全的

循环加锁,配合阻塞唤醒机制。我们发现,MessageQueue其实是【生产者-消费者】模型,Handler不断地放入消息,Looper不断地取出,这就涉及到死锁问题。如果Looper拿到锁,但是队列中没有消息,就会一直等待,而Handler需要把消息放进去,锁却被Looper拿着无法入队,这就造成了死锁,Handler机制的解决方法是循环加锁,代码在MessageQueue的next方法中:

Message next() {

...

for (;;) {

...

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {

...

}

}

}

我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。

8,Handler的阻塞唤醒机制是怎么实现的

Handler的阻塞唤醒机制是基于Linux的阻塞唤醒机制。这个机制也是类似于handler机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。

参考:Linux的阻塞唤醒机制

9,什么是Handler的同步屏障

所谓同步屏障,其实就是一个Message,只不过它是插入在MessageQueue的链表头,且其target==null。 而Message加急消息就是使用同步屏障实现的。同步屏障用到了postSyncBarrier()方法。

public int postSyncBarrier() {

return postSyncBarrier(SystemClock.uptimeMillis());

}

private int postSyncBarrier(long when) {

synchronized (this) {

final int token = mNextBarrierToken++;

final Message msg = Message.obtain();

msg.markInUse();

msg.when = when;

msg.arg1 = token;

Message prev = null;

Message p = mMessages;

// 把当前需要执行的Message全部执行

if (when != 0) {

while (p != null && p.when <= when) {

prev = p;

p = p.next;

}

}

// 插入同步屏障

if (prev != null) { // invariant: p == prev.next

msg.next = p;

prev.next = msg;

} else {

msg.next = p;

mMessages = msg;

}

return token;

}

}

可以看到,同步屏障就是一个特殊的target,即target==null,我们可以看到他并没有给target属性赋值,那这个target有什么用呢?

Message next() {

...

// 阻塞时间

int nextPollTimeoutMillis = 0;

for (;;) {

...

// 阻塞对应时间

nativePollOnce(ptr, nextPollTimeoutMillis);

// 对MessageQueue进行加锁,保证线程安全

synchronized (this) {

final long now = SystemClock.uptimeMillis();

Message prevMsg = null;

Message msg = mMessages;

/**

* 1

*/

if (msg != null && msg.target == null) {

// 同步屏障,找到下一个异步消息

do {

prevMsg = msg;

msg = msg.next;

} while (msg != null && !msg.isAsynchronous());

}

if (msg != null) {

if (now < msg.when) {

// 下一个消息还没开始,等待两者的时间差

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

} else {

// 获得消息且现在要执行,标记MessageQueue为非阻塞

mBlocked = false;

/**

* 2

*/

// 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取

if (prevMsg != null) {

prevMsg.next = msg.next;

} else {

mMessages = msg.next;

}

msg.next = null;

msg.markInUse();

return msg;

}

} else {

// 没有消息,进入阻塞状态

nextPollTimeoutMillis = -1;

}

// 当调用Looper.quitSafely()时候执行完所有的消息后就会退出

if (mQuitting) {

dispose();

return null;

}

...

}

...

}

}

我们重点看一下关于同步屏障的部分代码。

if (msg != null && msg.target == null) {

// 同步屏障,找到下一个异步消息

do {

prevMsg = msg;

msg = msg.next;

} while (msg != null && !msg.isAsynchronous());

}

如果遇到同步屏障,那么会循环遍历整个链表找到标记为异步消息的Message,即isAsynchronous返回true,其他的消息会直接忽视,那么这样异步消息,就会提前被执行了。同时,,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理。

10,IdleHandler的使用场景

前面说过,当MessageQueue没有消息的时候,就会阻塞在next方法中,其实在阻塞之前,MessageQueue还会做一件事,就是检查是否存在IdleHandler,如果有,就会去执行它的queueIdle方法。

IdleHandler看起来好像是个Handler,但他其实只是一个有单方法的接口,也称为函数型接口。

public static interface IdleHandler {

boolean queueIdle();

}

事实上,在MessageQueue中有一个List存储了IdleHandler对象,当MessageQueue没有需要被执行的Message时就会遍历回调所有的IdleHandler。所以IdleHandler主要用于在消息队列空闲的时候处理一些轻量级的工作。

因此,IdleHandler可以用来进行启动优化,比如将一些事件(比如界面view的绘制、赋值)放到onCreate方法或者onResume方法中。但是这两个方法其实都是在界面绘制之前调用的,也就是说一定程度上这两个方法的耗时会影响到启动时间,所以我们可以把一些操作放到IdleHandler中,也就是界面绘制完成之后才去调用,这样就能减少启动时间了。

11,HandlerThread使用场景

首先,我们来看一下HandlerThread的源码:

public class HandlerThread extends Thread {

@Override

public void run() {

Looper.prepare();

synchronized (this) {

mLooper = Looper.myLooper();

notifyAll();

}

Process.setThreadPriority(mPriority);

onLooperPrepared();

Looper.loop();

}

可以看到,HandlerThread是一个封装了Looper的Thread类,就是为了让我们在子线程里面更方便的使用Handler。这里的加锁就是为了保证线程安全,获取当前线程的Looper对象,获取成功之后再通过notifyAll方法唤醒其他线程,那哪里调用了wait方法呢?答案是getLooper方法。

public Looper getLooper() {

if (!isAlive()) {

return null;

}

// If the thread has been started, wait until the looper has been created.

synchronized (this) {

while (isAlive() && mLooper == null) {

try {

wait();

} catch (InterruptedException e) {

}

}

}

return mLooper;

}

本文参与了 SegmentFault 思否征文「如何“反杀”面试官?」,欢迎正在阅读的你也加入。

以上是 Android Handler面试总结 的全部内容, 来源链接: utcz.com/z/267642.html

回到顶部