0%

Handler常见面试题

前言

Handler机制是面试官非常喜欢问的知识点,本文主要是记录整理Handler相关面试题和解答。

目录

1、Handler:是什么?有什么用?为什么要用Handler,不用行不行?

Android定义的一套 线程(子线程与主线程)间通讯的消息传递机制 。把子线程中的 UI更新信息,传递给主线程(UI线程),以此完成UI更新操作。Android建议要求:我们在主线程(UI)线程中更新UI。Android的UI控件不是线程安全的,用Handler在多个线程并发更新UI的同时,保证线程的安全。

2、真的只能在主(UI)线程中更新UI吗?子线程一定不能更新UI吗?

并不是的,子线程更新UI也行,但是只能更新自己创建的View!只有创建这个view的线程才能操作这个view,但不建议在子线程更新UI。Android的UI更新被设计成了单线程。ViewRootImp 在 onCreate() 时还没创建;在 onResume()时,即ActivityThread 的 handleResumeActivity() 执行后才创建,调用 requestLayout(),走到 checkThread() 时就报错了。

3、真的不能在主(UI)线程中执行网络操作吗?

把 StrictMode 的网络检测关了,就可以在主线程中执行网络操作了,不过一般是不建议这样做的。

4、谈谈消息机制Handler作用 ?有哪些要素 ?流程是怎样的 ?

作用:负责线程间通信,这是因为在主线程不能做耗时操作,而子线程不能更新UI

四大要素:

Message:需要被传递的消息,消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息。

MessageQueue:负责消息的存储与管理,负责管理由 Handler发送过来的Message。

Handler:负责Message的发送及处理。主要向消息池发送各种消息事件(Handler.sendMessage())和处理相应消息事件(Handler.handleMessage())。

Looper:负责关联线程以及消息的分发,在该线程下从 MessageQueue获取 Message,分发给Handler,Looper创建的时候会创建一个 MessageQueue,调用loop()方法的时候消息循环开始,其中会不断调用messageQueue的next()方法,当有消息就处理,否则阻塞在messageQueue的next()方法中。当Looper的quit()被调用的时候会调用messageQueue的quit(),此时next()会返回null,然后loop()方法也就跟着退出。

具体流程:

在主线程创建的时候会创建一个Looper,同时也会在在Looper内部创建一个消息队列。而在创键Handler的时候取出当前线程的Looper,并通过该Looper对象获得消息队列,然后Handler在子线程中通过MessageQueue.enqueueMessage在消息队列中添加一条Message。

通过Looper.loop() 开启消息循环不断轮询调用 MessageQueue.next(),取得对应的Message并且通过Handler.dispatchMessage传递给Handler,最终调用Handler.handlerMessage处理消息。

5、一个线程能否创建多个Handler,Handler跟Looper之间的对应关系 ?
  • 一个Thread可以有多个Handler,但只能有一个Looper,一个MessageQueue。
  • 以一个线程为基准,他们的数量级关系是: Thread(1) : Looper(1) : MessageQueue(1) : Handler(N)

看Looper的创建,是在prepare()方法里。在创建之前去判断looper是否存在,存在就会抛出Only one Looper may be created per thread异常,这是在告诉我们一个线程只能有一个Looper。而TreadLocal的作用就是线程间隔离,确保一个线程对应一个Looper。然后在Looper构造方法中,初始化了一个MessageQueue。所以不管一个线程有多少个Handler,它们相关联的都是同一个Looper和MessageQueue。

6、Message可以如何创建?哪种效果更好,为什么?
  • 直接生成实例Message m = new Message()
  • 通过Message m = Message.obtain()
  • 通过Message m = mHandler.obtainMessage()

建议使用后两者效果更好,默认的消息池中消息数量是50,后两者是直接在消息池中取出一个Message实例,这样做就可以避免多生成Message实例。

7、为什么子线程中不可以直接new Handler()而主线程中可以?

因为在主线程启动时,主线程的Looper在ActivityThread中就通过prepareMainLooper() 完成了初始化。而子线程还需要调用手动调用 Looper.prepare()初始化Looper 和 Looper.loop()开启轮询。所以要在子线程创建Handler要先创建Looper,并开启Looper循环。主线程与子线程不共享同一个Looper实例。

8、主线程给子线程的Handler发送消息怎么写?

多线程并发的问题,当主线程执行到 sendEnptyMessage 时,子线程的 Handler 可能还没有创建,解决方法是:主线程延时给子线程发消息或者使用 HandlerThread 异步类。HandlerThread提供了主线程向子线程的通信。

9、HandlerThread实现的核心原理?

HandlerThread = 继承Thread + 封装Looper + Handler

源码分析:

HandlerThread的核心原理就是:

  • 继承Thread,getLooper()加锁死循环wait()堵塞;
  • run()加锁等待Looper对象创建成功,notifyAll()唤醒;
  • 唤醒后,getLooper返回由run()中生成的Looper对象;

总结:

  • HandlerThread本质上是一个线程类,它继承了Thread。

  • HandlerThread有自己内部的Looper对象,可以进行Looper循环。

  • 通过获取HandlerThread的Looper对象传递给Handler对象,可以在handlerMessage方法中执行异步任务。

  • 优点是不会有堵塞,多次创建和销毁子线程是很耗费资源的,减少对性能的消耗,缺点是不能进行多任务的处理,需要等待进行处理,处理效率较低。

  • 与线程池注重并发不同,HandlerThread是一个串行队列,HandlerThread背后只有一个线程。

  • 在 HandlerThread 不使用的时候,需要调用退出方法quit()/quitSafely(),停止Looper。

10、Looper是怎么拣队列里的消息的?

由 Looper -> loop函数->MessageQueue -> next函数:

关键其实就是:nextPollTimeoutMillis,决定了堵塞与否,以及堵塞的时间,三种情况:

  1. 等于0时,不堵塞,立即返回,Looper第一次处理消息,有一个消息处理完 ;
  2. 大于0时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行);
  3. 等于-1时,无消息时,会一直堵塞;

此处用到了Linux的pipe/epoll机制:没有消息时阻塞线程并进入休眠释放cpu资源,有消息时唤醒线程;

11、分发给Handler的消息是怎么处理的?

通过MessageQueue的queue.next()拣出消息后,调用msg.target.dispatchMessage(msg)
把消息分发给对应的Handler,调用 dispatchMessage() 方法。

回调优先级:

  1. Message 的回调方法:message.callback.run(),优先级最高;
  2. Handler.Callback 的回调方法:Handler.mCallback.handleMessage(msg),返回 true 时不会轮到步骤3,优先级仅次于1;
  3. Handler 自身的默认方法:Handler.handleMessage(msg),优先级最低。
12、IdleHandler是什么?

在 MessageQueue 类中有一个 static 的接口 IdleHanlder。当线程将要进入堵塞,以等待更多消息时,会回调这个接口;简单点说:当MessageQueue中无可处理的Message时回调。

作用:UI线程处理完所有View事务后,回调一些额外的操作,且不会堵塞主进程;

接口中只有一个 queueIdle() 函数,线程进入堵塞时执行的额外操作可以写这里,返回值是true的话,执行完此方法后还会保留这个IdleHandler,否则删除。使用方法:

13、Looper在主线程中死循环,为啥不会ANR?
  1. 主线程的主要方法就是消息循环,一旦退出消息循环,那么你的应用也就退出了,Looer.loop() 方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常。

  2. 造成ANR的不是主线程阻塞,而是主线程的Looper消息处理过程发生了任务阻塞,无法响应手势操作,不能及时刷新UI。

  3. 阻塞与程序无响应没有必然关系,虽然主线程在没有消息可处理的时候是阻塞的,但是只要保证有消息的时候能够立刻处理,程序是不会无响应的。

  4. 真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,Looper.loop() 本身不会导致应用卡死。

  5. Looper通过queue.next()获取消息队列消息,当队列为空,会堵塞,此时主线程也堵塞在这里,好处是:main函数无法退出,APP不会一启动就结束!

    application启动时,可不止一个main线程,还有其他两个Binder线程:ApplicationThreadActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。

  • 当系统受到因用户操作产生的通知时,会通过 Binder 方式跨进程通知 ApplicationThread;

  • 它通过Handler机制,往 ActivityThread 的 MessageQueue 中插入消息,唤醒了主线程;

  • queue.next() 能拿到消息了,然后 dispatchMessage 完成事件分发。

    死循环不会ANR,但是 dispatchMessage 中又可能会ANR哦!如果你在此执行一些耗时操作,导致这个消息一直没处理完,后面又接收到了很多消息,堆积太多,从而引起ANR异常。

14、主线程的死循环一直运行是不是特别消耗CPU资源呢?

其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

15、Handler导致内存泄露的原因及正确写法

泄露原因:非静态内部类默认持有外部类引用 + 生命周期不一致

当Handler消息队列还有未处理的消息 或 正在处理消息而外部类需要销毁时,将使得外部类无法被垃圾回收器GC回收,从而造成内存泄露

当Handler消息队列还有未处理的消息 / 正在处理消息,而外部类需销毁时,存在引用关系“未被处理 / 正处理的消息Message -> Handler实例 -> 外部类Activity实例”,将使得外部类无法被垃圾回收器GC回收,从而造成内存泄露。

Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 无法被GC导致泄露。是因为 Message 会持有 Handler,由于 Java 的特性,非静态内部类 / 匿名内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。在Java中,非静态内部类 / 匿名内部类会持有一个外部类的隐式引用,可能会造成外部类无法被GC。

解决方案有两种:

  • 1.静态内部类+弱引用

    静态内部类默认不持有外部类的引用,将 Handler 定义成静态内部类,在内部持有Activity的弱引用。而单单使用静态内部类,Handler就不能调用Activity里的非静态方法了,所以加上弱引用持有外部Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static class MyHandler extends Handler {
//创建一个弱引用持有外部类的对象
private final WeakReference<MainActivity> context;

private MyHandler(MainActivity context) {
this.context = new WeakReference<MainActivity>(context);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = context.get();
if (activity != null) {
switch (msg.what) {
case 0: {
activity.notifyUI();
}
}
}
}
}
  • 2.当外部类结束生命周期时,清空Handler内消息队列的消息

    在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)移除所有消息。使得 Handler 的生命周期(即消息存在的时期)与外部类的生命周期同步。

1
2
3
4
5
6
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
// 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
}

使用建议:为了保证Handler中消息队列中的所有消息都能被执行,推荐使用解决方案1解决内存泄露问题,即 静态内部类 + 弱引用的方式

16、Handler中的同步屏障机制

作用:用来处理紧急的事件消息,View的刷新(16ms刷新)等。

我们知道用Handler发送的Message后,MessageQueue的enqueueMessage()按照 时间戳升序 将消息插入到队列中,而Looper则按照顺序,每次取出一枚Message进行分发,一个处理完到下一个。

这时候,问题来了:有一个紧急的Message需要优先处理怎么破?

Handler中加入了「同步屏障」这种机制,来实现「异步消息优先执行」的功能。

添加一个异步消息的方法很简单:

1、Handler构造方法中传入async参数,设置为true,使用此Handler添加的Message都是异步的;

2、创建Message对象时,直接调用setAsynchronous(true)

可以通过 MessageQueue 的 postSyncBarrier 函数来开启同步屏障:往消息队列合适的位置插入了同步屏障类型的Message (target属性为null),接着,在 MessageQueue 执行到 next() 函数时:

遇到target为null的Message,说明是同步屏障,循环遍历找出一条异步消息,然后处理。

在同步屏障没移除前,只会处理异步消息,处理完所有的异步消息后,就会处于堵塞。如果想恢复处理同步消息,需要调用 removeSyncBarrier() 移除同步屏障。

在API 28的版本中,postSyncBarrier()已被标注@hide被隐藏,如果要调用的话只能通过反射机制进行调用。在系统源码可找到相关应用,为了更快地响应UI刷新事件,在ViewRootImpl的scheduleTraversals函数中就用到了同步屏障:

17、当Activity有多个Handler的时候,Message消息是否会混乱?怎么样区分当前消息由哪个Handler处理?

不会混乱,哪个Handler发送的消息,到时候也是这个handler处理。在发送消息的时候,msg会绑定target,这个target就是Handler本身,在循环取出消息时会调用msg.target.dispatchMessage(msg)分发处理消息,这里的target就是当时发送消息绑定的 handler 。

18、在子线程发送消息,却能够在主线程接收消息,主线程和子线程是怎么样切换的?

在子线程中用主线程 handler 发送消息,发送的消息被送到与主线程相关联的MessageQueue,也是主线程相关联的Looper在循环消息,handler所关联的是主线程的Looper和MessageQueue,所以最后消息的处理逻辑也是在主线程。只有发送消息是在子线程,其他都是在主线程。Handler与哪个线程的Looper相关联,消息处理逻辑就在与之关联的线程中执行,相应的消息的走向也就在相关联的MessageQueue中。所以子线程切换到主线程是很自然的过程,并没有想象中的复杂。

Handler消息机制用于同进程的线程间通信,其核心是线程间共享内存地址空间。即Handler实例对象mHandler位于线程间共享的内存堆上,工作线程与主线程都能直接使用该对象,只需要注意多线程的同步问题。

引用文章:

关于Handler 的这 15 个问题,你都清楚吗?

Android 消息机制——你真的了解Handler?