loader

Nerio News Magazine brings you trusted, timely and thought-provoking stories from around the globe.

Follow Us

Android 面试高频 25 问:Handler、Binder、四大组件与 Compose

Share This Article:

Android 面试的原理题有非常稳定的核心区。本文逐题给出标准答法、追问陷阱与面试官想听的那句话,覆盖 Handler 为什么不会 ANR、Binder 为什么只需一次拷贝、启动模式差异、后台限制,以及 Compose 的状态提升。

Android 面试高频 25 问:Handler、Binder、四大组件与 Compose
关键词Android 面试、Handler 机制、Binder、Activity 生命周期、Jetpack Compose

Android 面试的”原理题”有非常稳定的核心区:四大组件、Handler、Binder、View 绘制、内存泄漏、Compose。这些内容不随版本剧烈变动,但考察深度差异很大——同样问”Handler 原理”,有人说到 Looper 就停了,有人能讲清为什么主线程 Looper 死循环不会 ANR

本文按高频考点组织,每题给出”标准答法 + 追问陷阱 + 面试官想听的那句话”。版本背景:Android 17 已 GA,两端 target SDK 的年度升级窗口同时生效。

Handler 机制的四个角色
Handler 机制:Message → MessageQueue → Looper → Handler

一、Handler:面试的第一道分水岭

Q1:Handler 机制涉及哪几个类?各自做什么?

四个角色必须说全:

  • Message:消息载体。what/arg1/obj/target,以及用于复用的消息池obtain() 从链表中取,避免频繁创建对象)。
  • MessageQueue:按 when(执行时间)排序的单向链表。注意是链表不是队列,插入是按时间做有序插入。
  • Looper:持有 MessageQueue,loop() 方法里死循环取消息并分发。每个线程最多一个 Looper,用 ThreadLocal 保证线程隔离。
  • Handler:发送消息到 MessageQueue,并在 dispatchMessage() 里处理。
// loop() 的核心逻辑(简化)
public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;
    for (;;) {
        Message msg = queue.next();   // 没消息时在这里阻塞(nativePollOnce)
        if (msg == null) return;
        msg.target.dispatchMessage(msg);
        msg.recycleUnchecked();        // 回收进消息池
    }
}

Q2:主线程的 Looper 死循环为什么不会 ANR?

这是 Handler 最经典的追问,答不上来基本判定为”背过但没理解”

关键点:ANR 不是”卡住”,而是”该处理的事件没在规定时间内处理完”。主线程 Looper 的死循环,在没有消息时是通过 nativePollOnce 进入 epoll 等待睡眠的,不消耗 CPU;有消息到来时被唤醒继续执行。所谓 ANR,是某条消息(如 INPUT 事件、BroadcastReceiver.onReceive)执行超过阈值(输入事件 5s、前台广播 10s、Service 20s),而非循环本身。

一句话版:死循环在”等消息”,不在”忙等”;唤醒靠 Linux 的 epoll 机制;ANR 是单条消息执行超时,不是循环导致的。

Q3:Handler.postDelayed 是精确延时吗?

不是。它只是在 MessageQueue 里按 when = 当前时间 + delay 插入,到点后能否立即执行取决于前面有没有积压的消息。如果主线程正被一个耗时操作占着,延时会被顺延。此外系统还可能因为对齐唤醒(Alarm alignment)、Doze 模式做批量延迟。

需要精确计时的场景(如倒计时)应当用系统时间戳实时计算剩余值,而不是累加 postDelayed 的 delay。

Q4:Handler 引起内存泄漏的原因与解法?

泄漏链:延时消息 msg.target 持有 Handler → Handler 是非静态内部类,隐式持有 Activity → Activity 已 finish 但消息还在队列里 → Activity 无法回收。

// 反例:非静态内部类 + 延时消息
class MainActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())   // 隐式持有 Activity
    override fun onCreate(...) {
        handler.postDelayed({ updateUI() }, 60_000)
    }
}

// 正例1:静态内部类 + 弱引用
class SafeHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
    private val ref = WeakReference(activity)
    override fun handleMessage(msg: Message) {
        ref.get()?.updateUI()
    }
}

// 正例2:生命周期感知,销毁时清空消息
override fun onDestroy() {
    super.onDestroy()
    handler.removeCallbacksAndMessages(null)   // 关键:移除所有回调
}

二、Binder:Android 的 IPC 心脏

Q5:为什么 Android 用 Binder 而不用 Socket 或共享内存?

从三个维度对比:

维度 Binder Socket 共享内存
拷贝次数 1 次 2 次 0 次
安全性 内核校验 UID/PID 依赖上层协议 无访问控制
使用模型 C/S 架构,面向对象调用 字节流 需自行处理同步

要点展开:

  • 性能:Binder 通过 mmap 把内核缓冲区同时映射到内核空间和服务端用户空间,数据只需从发送方用户空间拷贝一次到内核,接收方直接读映射区。共享内存虽然零拷贝,但需要自己解决并发同步,且没有访问控制。
  • 安全:Binder 驱动在内核态记录调用方的 UID/PID,服务端可以据此做权限校验——这是 Android 权限体系的基石。Socket 和共享内存拿不到可靠的身份标识。
  • 易用性:Binder 是面向对象的(AIDL 生成的代理模式),调用远程方法像调用本地方法。

Q6:Binder 通信的完整流程?

  1. 注册:Server 向 ServiceManager(本身也是一个 Binder 服务,handle = 0)注册服务。
  2. 获取:Client 向 ServiceManager 查询,拿到服务的代理对象(BinderProxy)。
  3. 调用:Client 调用代理方法,数据打包成 Parcel,通过 transact() 发给 Binder 驱动。
  4. 驱动转发:驱动根据 handle 找到目标进程,拷贝一次数据,把请求加入服务端的 todo 队列,唤醒服务端 Binder 线程。
  5. 执行与返回:服务端在 onTransact() 中解包执行,结果沿原路返回。

同步调用时客户端线程会阻塞,所以要避免在主线程调用耗时远程方法。

Q7:AIDL 的 onewayin/out/inout 是什么?

  • oneway:异步调用,立即返回不阻塞,也不接收返回值。适用于”通知”型接口。
  • in:数据只从客户端流向服务端(默认)。
  • out:数据只从服务端写回客户端,客户端传入的对象内容不会被读取。
  • inout:双向。代价是两次序列化开销,实践中很少用

三、四大组件与生命周期

Q8:Activity 启动模式有哪几种?

  • standard:默认,每次都新建实例,压入启动它的那个任务栈。
  • singleTop栈顶复用。若目标已在栈顶则复用,回调 onNewIntent()。适合防止重复点击打开同一个页面。
  • singleTask栈内复用。若栈中已有实例,则把它上面的所有 Activity 全部出栈(clearTop),并回调 onNewIntent()。适合主页、登录页这类全局唯一的入口。
  • singleInstance独占一个任务栈,系统内全局唯一。适合来电界面、锁屏这类需要独立存在的页面。

追问”taskAffinity 有什么用“——它指定 Activity 归属的任务栈名,与 singleTaskallowTaskReparenting 配合使用时才生效,可以实现”把某个 Activity 挪到另一个任务栈”。

Q9:横竖屏切换时 Activity 的生命周期?

默认情况下会销毁重建onPause → onStop → onDestroy → onCreate → onStart → onResume

若配置 android:configChanges="orientation|screenSize",则不重建,只回调 onConfigurationChanged()

现代答法:不要靠 configChanges 逃避重建,而应该用 ViewModel 正确保存状态。ViewModel 的生命周期独立于 Activity 的配置变更,数据在重建后自动保留。这也是 Google 官方推荐的架构方式——把”配置变更”当成一次正常的生命周期事件来处理,而不是绕过它

Q10:Service 的两种启动方式有什么区别?

维度 startService bindService
生命周期 独立,需显式 stopService/stopSelf 随调用方,全部解绑即销毁
通信 单向,无直接回调 可通过 Binder 双向通信
典型场景 下载、播放(长期后台) 获取服务实例、跨进程调用

高频追问:后台限制。Android 8.0 起禁止后台应用创建后台服务,长期任务必须用前台服务(startForegroundService + 常驻通知),或改用 WorkManager 处理可延迟的后台工作。

Q11:BroadcastReceiver 的两种注册方式?

  • 静态注册(Manifest):应用未启动也能收到,但 Android 8.0 起大部分隐式广播被禁止静态注册(豁免列表内的除外)。
  • 动态注册registerReceiver):跟随组件生命周期,必须在 onDestroy/onStop 中反注册,否则内存泄漏。

四、View 绘制与性能

Q12:View 的绘制流程?

三个依次进行的阶段,由 ViewRootImpl 驱动:

  1. measure:递归测量,父 View 通过 MeasureSpec(模式 + 尺寸)把约束传给子 View,子 View 计算出自己的宽高并调用 setMeasuredDimension()
  2. layout:父 View 调用子 View 的 layout(l,t,r,b),确定四个顶点位置。
  3. draw:依次绘制背景 → 自身(onDraw)→ 子 View(dispatchDraw)→ 滚动条等装饰

MeasureSpec 三种模式必须记住:EXACTLY(精确值或 match_parent)、AT_MOST(wrap_content,上限为父的剩余空间)、UNSPECIFIED(无约束,常见于 ScrollView 内部、ListView 测量)。

Q13:为什么自定义 View 的 wrap_content 失效?

因为 View 的默认 onMeasure() 实现里,只要不是 EXACTLY 模式,就直接把父容器给的最大可用尺寸当成自己的尺寸——于是 wrap_content 表现得和 match_parent 一样。解法是重写 onMeasure,在 AT_MOST 模式下自己算出内容所需尺寸setMeasuredDimension()

Q14:UI 卡顿的原因与排查?

根因就两类:主线程做了耗时操作(IO、大量计算、JSON 解析、数据库查询),或绘制本身太重(布局层级过深、过度绘制、频繁触发 requestLayout)。

排查手段:Systrace / Perfetto 看主线程帧耗时、Layout Inspector 看层级、GPU 过度绘制调试看叠加层数、Choreographer.FrameCallback 做线上帧率监控、BlockedCanary 之类工具抓慢方法。

五、Jetpack Compose:2026 年的必考区

Q15:Compose 和传统 View 体系的根本区别?

  • 声明式:UI 是状态的函数 UI = f(state),状态变了自动重组,不需要手动 setText()
  • 无 View 实例:Compose 用 LayoutNode 树而非 View 树,没有 findViewById,也不需要 ViewHolder
  • 智能重组:只有读取了变化状态的可组合项才会重组,未读取的部分会被跳过(这是”重组作用域”的意义)。

Q16:什么是”状态提升”(State Hoisting)?

把状态从可组合项内部移到调用方,让可组合项变成无状态(stateless)的纯函数:参数进、事件出。

// 反例:状态藏在内部,调用方无法控制、难以复用和测试
@Composable
fun Counter() {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) { Text("$count") }
}

// 正例:状态提升,组件变成无状态的
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) { Text("$count") }
}

// 调用方持有状态,单一数据源
@Composable
fun CounterScreen() {
    var count by remember { mutableIntStateOf(0) }
    Counter(count = count, onIncrement = { count++ })
}

价值:单一数据源、可复用、可预览、可测试,并且便于把状态放进 ViewModel 从而在配置变更后保留。

Q17:rememberrememberSaveable 的区别?

  • remember:只在重组之间保留,配置变更(旋转屏幕)或进程重建后会丢失。
  • rememberSaveable:把状态写入 Bundle能跨配置变更和进程重建恢复。要求对象可放入 Bundle,或用自定义 Saver

六、小结

Android 原理题的考察主线是“机制 + 取舍 + 踩坑”:Handler 考的是消息循环与线程模型,Binder 考的是跨进程通信在性能与安全上的权衡,组件考的是生命周期与后台限制,Compose 考的则是有没有真正从命令式思维转到声明式思维

准备策略:每条主线准备一个自己真踩过的坑——比如”延时消息导致 Activity 泄漏,最后用弱引用加 onDestroy 清理解决的”。这类具体经历比完整背诵源码更能拿到高分。


参考:Android 开发者指南Jetpack Compose 文档AOSP 架构文档。行为随 Android 版本演进,涉及后台限制与权限的部分请以目标 API 等级的官方文档为准。

标签

#Android 面试题#Handler#Binder#面试

Related Post

发表回复

Your email address will not be published.