Android面试 - Handler
handler机制,主要作用
①跨线程通信
Handler允许不同线程(如主线程与子线程)之间安全传递消息或任务。子线程通过Handler发送消息到主线程,主线程通过Handler接收并处理消息(如更新UI),从而避免直接在子线程操作UI导致的崩溃问题。
②任务调度与解耦
通过消息队列(MessageQueue)对任务进行排序和分发,实现线程任务的统一管理和执行。主线程负责处理UI交互任务,子线程处理耗时操作(如网络请求、数据库操作等),两者通过Handler机制高效解耦,提升应用响应性和稳定性。
③异常防护
主线程禁止执行耗时操作(超过5秒会触发ANR),子线程禁止直接更新UI。Handler机制通过消息传递方式,将耗时任务转移到子线程处理,同时确保UI更新操作在主线程安全执行,有效防止程序崩溃
Handler 消息通信机制怎么工作
Handler 并不是独立工作的,它依赖三个核心组件组成一个消息循环系统:
-
Message:传递的数据载体。
-
MessageQueue:一个按时间排序的单链表队列,存放待处理的
Message。 -
Looper:消息循环的驱动引擎,不断从
MessageQueue中取消息,分发给对应的Handler处理。 -
Handler:消息的发送器和处理器,既可以将消息塞入
MessageQueue,也可以处理从队列中取出的消息。
运行流程:
-
一个线程通过
Looper.prepare()创建自己的MessageQueue和Looper,然后调用Looper.loop()进入无限循环,不断从队列里取消息。 -
Handler在哪个线程创建,就会绑定那个线程的Looper和MessageQueue。 -
任意线程 都可以持有这个
Handler的引用,并调用它的sendMessage()或post()方法,把消息放入创建该 Handler 的那个线程的 MessageQueue。 -
目标线程的
Looper循环取出消息后,又会回调给同一个Handler的handleMessage()方法去处理。
关键点:消息的执行线程,由 Handler 创建时所在的线程决定,而不是消息发送的线程。这就实现了任意线程间的通信。
为什么必须从子线程切换到主线程?(核心原因)
1. 最根本的原因:Android UI 是单线程模型,且线程不安全
Android 制定了一条铁律:只有原始创建这个 View 视图树的线程(即主线程,也叫 UI 线程)才能访问和修改该 View。任何线程直接操作 UI 都会被系统检查并抛出 CalledFromWrongThreadException。
2. 主线程的特殊性与职责
主线程在应用启动时,就会自动执行 Looper.prepareMainLooper() 和 Looper.loop(),开启消息循环。它的核心职责就是处理所有 UI 事件和重绘:
-
处理用户输入(触摸、点击、键盘)。
-
执行
onCreate、onResume等生命周期回调。 -
测量、布局、绘制整个界面(
onMeasure、onLayout、onDraw)。 -
处理各种系统回调。
因此,任何可能影响 UI 的操作,最终都必须路由到主线程的消息队列中排队执行。
3. 为什么子线程需要“切”回去?
-
耗时任务(网络请求、文件读写、数据库查询、复杂计算)如果放在主线程执行,会阻塞消息循环,导致无法响应用户操作、无法刷新界面,最终触发 ANR(Application Not Responding)。
-
为了不卡住主线程,我们必须用子线程去做耗时工作。
-
但耗时工作一旦完成,拿到了结果(比如下载的图片、解析的数据),下一步就必须更新 UI 来展示给用户。这就产生了一个矛盾:
-
不能在主线程干耗时活。
-
不能在子线程更新 UI。
-
于是,用 Handler 把子线程的结果传递回主线程,就完美解决了这个矛盾:子线程只管干活,干完后把结果封装成 Message,通过主线程的 Handler 发送出去,主线程在其 handleMessage 中安全地拿到结果并更新 UI。
Handler 如何实现“从子线程切回主线程”
操作非常简单,因为主线程的 Looper 是全局唯一的 Looper.getMainLooper(),我们只需要用这个主线程的 Looper 来创建 Handler 即可:这样,即使 sendMessage 在子线程调用,但 handleMessage 的回调一定执行在主线程,保证了 UI 操作的安全。
延伸思考:其他方式也是同样道理
Android 提供的 Activity.runOnUiThread()、View.post() 等便捷方法,底层封装本质上依然是通过主线程的 Handler 来实现。
而现代的 LiveData、Flow 结合协程的 Dispatchers.Main,则是利用更上层的封装,自动处理了线程切换,但它们背后的根本哲学与 Handler 是完全一致的:耗时逻辑跑后台,UI 更新必然投递到主线程的队列上串行执行。
为什么这样是安全的:因为Handler将更新UI的操作封装成一个消息,并发送到主线程的消息队列,然后由主线程的Looper依次处理这些消息。这样,所有的UI操作都在主线程中顺序执行,避免了多线程操作UI的问题。
Handler工作流程
概述:Handler将Message发送到Looper的消息队列中,即MessageQueue,等待Looper循环读取Message,处理Message,然后调用Message的target,即附属Handler的dispatchMessage方法,将该消息回调到handleMessage方法中,然后完成UI更新。
Handler主要函数
MessageQueue主要函数

链表,优先级队列,时间排序
Q:Handler在子线程发送消息,在主线程处理消息,这里线程切换发生在哪个地方?
关键点: Handler 在哪个线程创建,就绑定哪个线程的 Looper
Handler 的线程切换不是发生在发送消息时,而是发生在消息被目标线程的 Looper 从 MessageQueue 中取出并分发的那一刻。Handler 只是"绑定"了目标线程的 Looper,真正的线程切换由 Looper 所在的线程决定。
子线程A
↓
handler.sendMessage(msg) // 仍在子线程
↓
handler.enqueueMessage() // 仍在子线程
↓
MessageQueue.enqueueMessage() // 仍在子线程
↓
消息进入队列 ← 这里跨线程共享
↓
主线程 Looper.loop() 取出消息
↓
主线程 Handler.dispatchMessage() // 切换到主线程
↓
主线程 Handler.handleMessage() // 在主线程执行
Q:消息延迟是如何实现的?
✅ 通过 nativePollOnce 实现精确等待
// 调用 nativePollOnce 等待
nativePollOnce(ptr, nextPollTimeoutMillis);
1、一个线程有几个handler?
答:多个,只要内存够,就可以new多个handler
2、一个线程有几个looper?如何保证?
一个looper,threadlocal保证
答:一个线程对应一个hashmap<key, value>,存在多个entry数组,但是key对应threadlocal(final的),value对应looper,在looper.prepare时进行threadlocal和looper绑定,map中key是唯一的,value可以是多个,在prepare时通过threadlocal的get判断是否looper已经创建,保证looper的唯一性,即value的唯一性
3. Handler内存泄漏原因?为什么其他的内部类没有说过有这个问题?
答:持有activity,内部类:持有外部类的对象引用
因为非静态内部类会隐式持有外部类实例的引用,当非静态内部类的引用的声明周期长于Activity的生命周期时,会导致Activity无法被GC正常回收掉
发送消息和处理消息的handler是一个,为什么子线程的handler发送的消息,主线程还可以用同一个handler处理消息
MessageQueue-->Message-->包含Handler-->持有activity
delay:Message:1min
1min后messagequeue才去处理message,即1min后message才解除Handler关系,和activity持有关系,msg没有被及时处理,activity就会被一直持有,即使activity调用ondestroy,但是由于activity被handler持有,所以得不到释放,造成内存泄漏
解决:handler静态内部类,弱引用方式
4. 为何主线程可以new Handler?如果想要在子线程中new Handler要做些什么准备?
启动app:点击桌面icon-->launcher-->zygote-->application(art,为每个app创建一个虚拟机)-->启动ActivityThread.main()函数,进行主线程looper初始化,所以我们可以在主线程new handler
而子线程中并没有创建lopper对象,所以需要先调用Looper.prepare(),之后再创建Handler对象,最后调用Looper.loop(),不断的读出消息。
5. 子线程中维护的looper,消息队列无消息的时候处理方案是什么?有什么用?
queue.next()
nativePollOnce(long ptr,时间)// 睡眠、等待、阻塞
时间:如果为-1,则表示无限等待(sleep,线程挂起),直到有事件发生为止。如果值为0,则无需等待立即返回
looper.quit(boolean)、nativeWake(long ptr)// 唤醒、recycleUncheck()// 将消息中变量置空
handler睡眠和唤醒机制
loop阻塞在queue.next(),有新消息到来或超时唤醒时,都会从这个函数中返回到MessageQueue.next()函数中的nativePollOnce(ptr, nextPollTimeoutMillis)处,继续往下执行。
当有新消息到达(通过Handler发送消息时,会调用nativeWake()唤醒等待的线程),epoll_wait返回,线程被唤醒并取出消息分发。
looper在等待期间是如何等待的?
Looper 在等待消息时,并非空转轮询(忙等),而是依靠一种高效的 I/O 多路复用机制,让线程在没有消息时进入休眠状态,几乎不消耗 CPU;当新消息抵达时,又能被立即精准唤醒。
主线程需要释放么?——绝对不能,quit函数中会有主线程相关判断,报异常
6. 既然可以存在多个handler往msgqueue中添加数据(发消息时各个handler可能处于不同线程),那它内部是如何确保线程安全的?
一个线程对应一个looper,一个looper对应一个msgqueue,一个线程对应一个msgqueue
加锁(内置锁):synchronized(this),这里的this代表msgqueue,(synchronized可以修饰函数,静态函数,代码块)
7. 我们使用msg时应该如何创建它?
obtain方法,享元设计模式(bitmap中在使用),消息处理完只清空此块内存里面的变量值,不释放此块内存,下次使用直接改变此块内存中变量值即可,防止了一直new--destroy
建议使用handler.obtainMessage / Message.obtain()获取Message对象,因为Message维护着一个消息池,这个消息池的数据结构是单向链表,优先从池子里拿数据,如果池子里没有再创建对象。如果Message对象已存在,可以使用obtain(msg)方法,最终也会调用obtain()
8. 不同的Handler如何与他的msg绑定的?
由于一个线程中可能有多个handler,为了区分这些不同的hanlder所需要处理的消息,每个Message对象都维护有一个hanlder实例即target,Message通过target属性标识handler。在loop方法中通过调用msg.target.dispathMessage(msg)方法将消息分发到与Message绑定handler.handleMessage()方法中。 所以一个handler发送的消息只会被自己接收
Looper会存在线程的哪里?
Looper会存在线程的ThreadLocal对象里,该对象是线程的缓存区
不同线程的handler如何将message发送到对应的messagequeue?
每个Handler都绑定到一个Looper,而Looper管理一个MessageQueue。当Handler发送消息时,消息会被放入关联的MessageQueue中,然后由Looper循环取出处理。
9. looper死循环为什么不会导致应用卡死
卡死是ANR,而looper没有消息需要处理时,就睡眠,主线程处于等待,cpu给其他线程使用
应用卡死,也就是ANR所产生的原因?
① 5秒钟之内没有响应输入的事件,比如按键、屏幕触摸等。
② 广播接收器在10秒内没有执行完毕
AMS管理机制
每一个应用都存在于自己的虚拟机中,也就是说每一个应用都有自己的一个main函数。
启动流程:launcher->zygote->art application->activityThread->main()
所有应用所有生命周期的函数(包括Activity、Service所有生命周期)都运行在这个Looper里面
而且,它们都是以消息的方式存在的
既然Handler的消息全都是loop来的,为什么我们没有ANR问题?
之前不是说5秒钟不响应就会出现阻塞问题吗,为什么休眠好长时间也并不会被ANR呢?
唤醒线程的方法:
1) looper中添加message. 通过nativeWait()->loop运作
2) 输入事件
产生ANR的问题不是因为主线程睡眠了,而是因为输入事件没有响应,输入事件没有响应他就没有办法唤醒这个Looper,才加了这个5秒的限制
ANR:消息没有及时处理,一旦超过5s,则产生ANR,导致消息队列阻塞

10. looper.loop为什么不会阻塞主线程
11. msg的数据结构是什么样子
基本数据字段:what、arg1、arg2、obj、data。
管理字段:when、target、callback、next、flags。
对象池机制:通过sPool维护消息池,复用实例。
单链表结构:通过next指针连接,形成消息队列。
线程安全:synchronized保证操作安全。
序列化支持:Parcelable接口。
12. 使用handler的postDelay后消息队列会有什么变化
Handler.postDelayed最终会调用sendMessageAtTime,将消息插入到MessageQueue中,并且设置消息的执行时间(when = 当前时间 + 延迟时间)。然后MessageQueue的enqueueMessage方法会根据这个when时间将消息插入到合适的位置。如果消息队列为空,或者新消息的when比队列头的时间更早,那么新消息会成为新的队列头,并且唤醒阻塞的Looper线程。否则,消息会被插入到队列中的适当位置,保持按时间排序。
13.为什么log日志会不打印
View view = new View(this);
view.post(new Runnable() {
@Override
public void run() {
Log.i(TAG, "[view.post] >>>> 1 ");
}
});
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
} // Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
即:当执行 View.post 方法时,如果 AttachInfo 不为空,则通过 AttachInfo 的 Handler 来执行 Runnable;否则,将这个 Runnable 抛到 View 的执行队列 HandlerActionQueue 中,也就是只有当 View attch 到 Window 后,才会给 AttachInfo 赋值。所以,在 示例 里的代码会直接走入 getRunQueue().post(action)由于 View view = new View(this); 没有将view attch到window上,所以执行的 View.post 方法将可执行请求都缓存到请求队列里示例 中的代码可改为:
View view = new View(this);
rootView.addView(view);
view.post(new Runnable() {
。。。。。。
}
https://juejin.cn/post/6922022848756219918?from=search-suggest
https://www.cnblogs.com/chenxibobo/p/14086899.html
14.View的onAttachedToWindow和onDetachedFromWindow的调用时机分析:
https://www.jianshu.com/p/e7b6fa788ae6
15.handler机制 looper插入消息执行消息如何工作 一直执行为什么不会出问题?
因为Android是事件驱动的,Looper就是需要不断的循环去处理消息,才能让主线程不会运行结束而导致APP应用进程结束;
而ANR的产生是因为主线程Looper在分发消息之前会发送一个延迟消息,这个延迟消息一旦执行就会抛出ANR异常,当消息在规定时间内完成时会移除这个延迟消息,超时则会执行并抛出ANR异常,所以只有在主线程处理消息时占用太多时间才会导致ANR,死循环是不会导致ANR异常的。
而且当消息队列为空时,会调用nativePollonce方法阻塞当前队列,直到有新的消息或者延迟消息时间到了才会唤醒当前线程
MessageQueue.enqueueMessage
Looper.loop()
👇🏻
queue.next()
msg.target.dispatchMessage(msg)
需要注意的是,如果当前消息队列里没有消息,queue.next()方法会阻塞住而并不是返回null。那么什么时候才是null退出循环呢?只有调用Looper.quit()或Looper.quitSafely()方法,这样就会调用MessageQueue的quit/quitSafely来通知消息队列退出,这时queue.next()就会返回null了
16.关于Handler同步屏障你可能不知道的问题:
https://juejin.cn/post/6940607471153053704?searchId=202509191817367F63D2A89226C830EE72
17.handler机制 消息队列里如何排序 比如postDelay和sendMessageAtTime/sendMessageDelayed怎么放到对应位置?
消息都是根据它们的when字段来排序的。when字段是一个长整型值,队列会按照这些时间点从小到大排序消息入栈时,首先会判断新消息如果是第一个消息 或者 新消息没有延迟 或者 新消息延迟时间小于队列第一个消息的,都会立刻对这个消息进行处理。只有当消息延迟大于队列头消息时,才会依次遍历消息队列,将消息按延迟时间插入消息队列相应位置。
Handler提供的指定处理时间的api诸如postDelayed()/postAtTime()/sendMessageDelayed()/sendMessageAtTime(),只能保证在指定时间之前不被执行,不能保证在指定时间点被执行。
18.handler.postdelay和view.postdelay区别
https://juejin.cn/post/6939763855216082974?from=search-suggest
https://cloud.baidu.com/article/3548740
https://blog.csdn.net/2401_83601703/article/details/136994177
IdleHandler:主要用于在消息队列空闲的时候处理一些轻量级的工作
handler相关问答:https://blog.51cto.com/u_16213723/7471131
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
} // Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
意思是将任务交由attachInfo中的Handler处理,保证在UI线程执行。从本质上说,它还是依赖于以Handler、Looper、MessageQueue、Message为基础的异步消息处理机制。相对于新建Handler进行处理更加便捷。因为attachInfo中的Handler其实是由该View的ViewRootImpl提供的,所以post方法相当于把这个事件添加到了UI 事件队列中。下面举一个常用的例子,比如在onCreate方法中获取某个view的宽高,而直接View#getWidth获取到的值是0。要知道View显示到界面上需要经历onMeasure、onLayout和onDraw三个过程,而View的宽高是在onLayout阶段才能最终确定的,而在Activity#onCreate中并不能保证View已经执行到了onLayout方法,也就是说Activity的声明周期与View的绘制流程并不是一一绑定。那为什么调用post方法就能起作用呢?首先MessageQueue是按顺序处理消息的,而在setContentView()后,队列中会包含一条询问是否完成布局的消息,而我们的任务通过View#post方法被添加到队列尾部,保证了在layout结束以后才执行。
Handler主要场景是子线程完成耗时操作的过程中,通过 Handler向主线程发送消息Message,用来刷新UI界面
了解Handler的发送消息和处理消息的源码实现
Handler源码的一个切入点就是它的默认构造器
从New Handler()开始


这个 Looper 是什么?何时被初始化?
Looper介绍
启动一个Java程序的入口函数是main方法,当main函数执行完毕之后此程序停止运行,也就是进程会自动终止。但是当打开一个Activity之后,只要不按下返回键Activity会一直显示在屏幕上,也就是Activity所在进程会一直处于运行状态
Looper 内部维护一个无限循环,保证 App进程持续进行
Looper初始化

if 这行代码的目的是确保在一个线程中Looper.prepare()方法只能被调用1次


下面截图写法会报错:不是说调用2次prepare才会抛异常吗?为什么MainActivity中只调用了1遍
就导致程序崩溃?

是因为在MainActivity所在进程被创建时,Looper的prepare方法已经在main方法中调用了1遍
这会直接导致一个非常重要的结果:
prepare()方法在一个线程中只能被调用1次
Looper 的构造方法在一个线程中只能被调用1次
最终导致 MessageQueue在一个线程中只会被初始化1次
Looper负责做什么事情
Looper做的事情是:不断从 MessageQueue中取出Message,然后处理Message中指定的任务


Handler的 sendMessage 方法
Handler的 enqueueMessage 方法

Looper.loop()为什么不会阻塞主线程
Looper中的 loop方法实际上是一个死循环,但是我们的UI线程却并没有被阻塞,反而还能够进行各种手势操作,这是为什么呢???
nativePollOnce方法是一个native方法,当调用此native方法时,主线程会释放CPU资源进入休眠状态,直到下条消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作

Handler的sendMessageDelayed或者postDelayed 是如何实现的?
在向MessageQueue队列中插入Message时,会根据Message的执行时间排序
而消息的延时处理的核心实现是在获取Message 的阶段。如下所示:
如果当前系统时间大于或等于Message.when,那么会返回Message给Looper.loop()
但是这个逻辑只能保证在when之前消息不被处理,不能够保证一定在when时被处理

总结
1. 应用启动是从ActivityThread的main 开始的
先是执行了Looper.prepare(),该方法先是new了一个Looper对象,在私有的构造方法中又创建了MessageQueue作为此Looper对象的成员变量,Looper对象通过ThreadLocal绑定MainThread 中
2. 当创建Handler子类对象时,在构造方法中通过ThreadLocal获取绑定的Looper对象,并获取此 Looper对象的成员变量MessageQueue作为该Handler对象的成员变量
3. 在子线程中调用上一步创建的Handler子类对象的sendMesage(msg)方法时,在该方法中将msg的target属性设置为自己本身,及handler本身,同时调用成员变量MessageQueue对象的enqueueMessage()方法将msg放入MessageQueue中
4. 主线程创建好之后,会执行Looper.loop()方法,该方法中获取与线程绑定的Looper对象,继而获取该Looper对象的成员变量MessageQueue对象,并开启一个会阻塞(不占用资源)的死循环,只要MessageQueue中有msg,就会获取该msg,并执行msg.target.dispatchMessage(msg)方法,(msg.target即上一步引用的handler对象),此方法中调用了第二步创建handler子类对象时复写的handleMessage()方法
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐





所有评论(0)