Framework源码面试之Handler面试集合

常面问题

1.Handler怎么在主线程和子线程进行数据交互的原理?

主线程和子线程通过handler交互,交互的载体是通过Message这个对象,实际上我们在子线程发送的所有消息,都会加入到主线程的消息队列中,然后主线程分发这些消息,这个就很容易做到俩个线程信息的交互。

看到这里,你可能有疑问了,我从子线程发送的消息,怎么就加到了主线程的消息队列里呢???

大家可以看看你自己的代码,你的handler对象是不是在主线程初始的?子线程发送消息,是不是通过这个handler发送的?

这就很简单了,handler只需要把发送的消息,加到自身持有的Looper对象的MessageQueue里面(mLooper变量)就ok了

所以,你在哪个线程里面初始化Handler对象,在不同的线程中,使用这个对象发送消息;都会在你初始化Handler对象的线程里分发消息。

2.Handler中主线程的消息队列是否有数量上限?为什么?

这问题整的有点鸡贼,可能会让你想到,是否有上限这方面?而不是直接想到到上限数量是多少?

解答Handler主线程的消息队列肯定是有上限的,每个线程只能实例化一个Looper实例(上面讲了,Looper.prepare只能使用一次),不然会抛异常,消息队列是存在Looper()中的,且仅维护一个消息队列

重点 :每个线程只能实例化一次Looper()实例、消息队列存在Looper

拓展MessageQueue类,其实都是在维护mMessage,只需要维护这个头结点,就能维护整个消息链表

3.Handler中有Loop死循环,为什么没有卡死?为什么没有发生ANR

先说下ANR :5秒内无法响应屏幕触摸事件或键盘输入事件;广播的onReceive()函数时10秒没有处理完成;前台服务20秒内,后台服务在200秒内没有执行完毕;ContentProviderpublish在10s内没进行完。所以大致上Loop死循环和ANR联系不大,问了个正确的废话,所以触发事件后,耗时操作还是要放在子线程处理,handler将数据通讯到主线程,进行相关处理。

线程实质上是一段可运行的代码片,运行完之后,线程就会自动销毁。当然,我们肯定不希望主线程被over,所以整一个死循环让线程保活。

为什么没被卡死 :在事件分发里面分析了,在获取消息的next()方法中,如果没有消息,会触发nativePollOnce方法进入线程休眠状态,释放CPU资源,MessageQueue中有个原生方法nativeWake方法,可以解除nativePollOnce的休眠状态,ok,咱们在这俩个方法的基础上来给出答案

  • 当消息队列中消息为空时,触发MessageQueue中的nativePollOnce方法,线程休眠,释放CPU资源

  • 消息插入消息队列,会触发nativeWake唤醒方法,解除主线程的休眠状态

    • 当插入消息到消息队列中,为消息队列头结点的时候,会触发唤醒方法
    • 当插入消息到消息队列中,在头结点之后,链中位置的时候,不会触发唤醒方法

综上:消息队列为空,会阻塞主线程,释放资源;消息队列为空,插入消息时候,会触发唤醒机制

  • 这套逻辑能保证主线程最大程度利用CPU资源,且能及时休眠自身,不会造成资源浪费

本质上,主线程的运行,整体上都是以事件(Message)为驱动的。

4.为什么不建议在子线程中更新UI?

多线程操作,在UI的绘制方法表示这不安全,不稳定。

假设一种场景:我会需要对一个圆进行改变,A线程将圆增大俩倍,B改变圆颜色。A线程增加了圆三分之一体积的时候,B线程此时,读取了圆此时的数据,进行改变颜色的操作;最后的结果,可能会导致,大小颜色都不对。。。

5.可以让自己发送的消息优先被执行吗?原理是什么?

这个问题,我感觉只能说:在有同步屏障的情况下是可以的。

同步屏障作用:在含有同步屏障的消息队列,会及时的屏蔽消息队列中所有同步消息的分发,放行异步消息的分发。

在含有同步屏障的情况,我可以将自己的消息设置为异步消息,可以起到优先被执行的效果。

6.子线程和子线程使用Handler进行通信,存在什么弊端?

子线程和子线程使用Handler通信,某个接受消息的子线程肯定使用实例化handler,肯定会有Looper操作,Looper.loop()内部含有一个死循环,会导致线程的代码块无法被执行完,该线程始终存在。

如果在完成通信操作,我们一般可以使用: mHandler.getLooper().quit() 来结束分发操作

说明下quit()方法进行几项操作

  • 清空消息队列(未分发的消息,不再分发了)
  • 调用了原生的销毁方法 nativeDestroy(猜测下:可能是一些资源的释放和销毁)
  • 拒绝新消息进入消息队列
  • 它可以起到结束loop()死循环分发消息的操作

拓展quitSafely() 可以确保所有未完成的事情完成后,再结束消息分发。

7.Handler中的阻塞唤醒机制?

这个阻塞唤醒机制是基于 Linux 的 I/O 多路复用机制 epoll 实现的,它可以同时监控多个文件描述符,当某个文件描述符就绪时,会通知对应程序进行读/写操作.

MessageQueue 创建时会调用到 nativeInit,创建新的 epoll 描述符,然后进行一些初始化并监听相应的文件描述符,调用了epoll_wait方法后,会进入阻塞状态;nativeWake触发对操作符的 write 方法,监听该操作符被回调,结束阻塞状态。

8.什么是IdleHandler?什么条件下触发IdleHandler

IdleHandler的本质就是接口,为了在消息分发空闲的时候,能处理一些事情而设计出来的

具体条件:消息队列为空的时候、发送延时消息的时候

9.消息处理完后,是直接销毁吗?还是被回收?如果被回收,有最大容量吗?

Handler存在消息池的概念,处理完的消息会被重置数据,采用头插法进入消息池,取的话也直接取头结点,这样会节省时间

消息池最大容量为50,达到最大容量后,不再接受消息进入

10.不当的使用Handler,为什么会出现内存泄漏?怎么解决?

先说明下,Looper对象在主线程中,整个生命周期都是存在的,MessageQueue是在Looper对象中,也就是消息队列也是存在在整个主线程中;我们知道Message是需要持有Handler实例的,Handler又是和Activity存在强引用关系

存在某种场景:我们关闭当前Activity的时候,当前Activity发送的Message,在消息队列还未被处理,Looper间接持有当前activity引用,因为俩者直接是强引用,无法断开,会导致当前Activity无法被回收

思路:断开俩者之间的引用、处理完分发的消息,消息被处理后,之间的引用会被重置断开

解决 :使用静态内部类弱引Activity、清空消息队列

Handler的作用: 当我们需要在子线程处理耗时的操作(例如访问网络,数据库的操作),而当耗时的操作完成后,需要更新UI,这就需要使用Handler来处理,因为子线程不能做更新UI的操作。Handler能帮我们很容易的把任务(在子线程处理)切换回它所在的线程。简单理解,Handler就是解决线程和线程之间的通信的。

Handler连环之说说Handler的作用,以及每个类让他们的角色

使用的handler的两种形式

1.在主线程使用handler; 2.在子线程使用handler。

Handler的消息处理主要有五个部分组成

Message

Handler

Message Queue

Looper

ThreadLocal

首先简要的了解这些对象的概念

  1. Message:Message是在线程之间传递的消息,它可以在内部携带少量的数据,用于线程之间交换数据。Message有四个常用的字段,what字段,arg1字段,arg2字段,obj字段。what,arg1,arg2可以携带整型数据,obj可以携带object对象。
  2. Handler :它主要用于发送和处理消息的发送消息一般使用sendMessage()方法,还有其他的一系列sendXXX的方法,但最终都是调用了sendMessageAtTime方法,除了sendMessageAtFrontOfQueue()这个方法 而发出的消息经过一系列的辗转处理后,最终会传递到Handler的handleMessage方法中。
  3. Message Queue: MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息,这部分的消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
  4. Looper :每个线程通过Handler发送的消息都保存在,MessageQueue中,Looper通过调用loop()的方法,就会进入到一个无限循环当中,然后每当发现Message Queue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

ThreadLocal: MessageQueue对象,和Looper对象在每个线程中都只会有一个对象,怎么能保证它只有一个对象,就通过ThreadLocal来保存。Thread Local是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储到数据,对于其他线程来说则无法获取到数据。

Handler连环泡之 说说 Looper 死循环为什么不会导致应用卡死?

​ 线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThreadActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。

首先我们看一段代码

java 复制代码
 new Thread(new Runnable() {
        @Override
        public void run() {
            Log.e("qdx", "step 0 ");
            Looper.prepare();

            Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();

            Log.e("qdx", "step 1 ");
            Looper.loop();

            Log.e("qdx", "step 2 ");

        }
    }).start();

我们知道Looper.loop();里面维护了一个死循环方法,所以按照理论,上述代码执行的应该是 step 0 -->step 1 也就是说循环在Looper.prepare();与Looper.loop();之间。

在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待(阻塞)状态,而如果退出Looper以后,这个线程就会立刻(执行所有方法并)终止,因此建议不需要的时候终止Looper

执行结果也正如我们所说,这时候如果了解了ActivityThread,并且在main方法中我们会看到主线程也是通过Looper方式来维持一个消息循环

java 复制代码
    public static void main(String[] args) {
        Looper.prepareMainLooper();//创建Looper和MessageQueue对象,用于处理主线程的消息
        ActivityThread thread = new ActivityThread();
        thread.attach(false);//建立Binder通道 (创建新线程)
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();
    
        //如果能执行下面方法,说明应用崩溃或者是退出了...
        throw new RuntimeException("Main thread loop unexpectedly exited");
      }

那么回到我们的问题上,这个死循环会不会导致应用卡死,即使不会的话,它会慢慢的消耗越来越多的资源吗?

​对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。
​主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loopqueue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Gityuan--Handler(Native层)

Handler连环泡之 说说 Looper 死循环为什么不会导致应用卡死?

事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:

java 复制代码
public static void main(String[] args) {
//创建Looper和MessageQueue对象,用于处理主线程的消息
 Looper.prepareMainLooper();

 //创建ActivityThread对象
 ActivityThread thread = new ActivityThread(); 

 //建立Binder通道 (创建新线程)
 thread.attach(false);

 Looper.loop(); //消息循环运行
 throw new RuntimeException("Main thread loop unexpectedly exited");
}

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:一旦退出消息循环,那么你的程序也就可以退出了。 从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。

thread.attach(false)方法函数中便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。「Activity 启动过程」

​ 比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;

再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。

相关推荐
找方案4 小时前
AI+人力资源:AI招聘面试的兴起与争议
人工智能·面试·职场和发展
CC数分11 小时前
2026年HRBP岗位硬性要求和加分项
面试·职场和发展·数据分析
2601_9626802211 小时前
数据分析面试常考的SQL题和业务题分别是哪几类?
sql·面试·数据分析
黄敬峰12 小时前
一文搞懂 Agentic RAG:用 LangGraph 打造会思考的智能检索架构
面试
晚安日记wanna12 小时前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
晚安日记wanna12 小时前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
晚安日记wanna12 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna13 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna13 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
晚安日记wanna13 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构