乐趣区

关于android: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;
    }
退出移动版