Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Android 系统原理

这一篇偏底层; 如果你已有操作系统或并发基础, 理解会更顺畅. 面试常问 Handler, Binder, 启动流程, 应结合自己的基础和项目经验组织回答.

一, Handler / Looper / MessageQueue

Android 的线程间通信与主线程消息循环核心机制.

  • Looper: 每个线程最多一个 Looper, 内部持有 MessageQueue, loop() 死循环不断取消息分发. 主线程的 Looper 由系统在 ActivityThread.main 中创建.
  • MessageQueue: 消息队列, 按时间排序的单链表; next() 取消息, 无消息时通过 epoll 阻塞 (不耗 CPU).
  • Handler: 发送 (sendMessage/post) 和处理 (handleMessage) 消息; Message 持有 target (发送它的 Handler).
  • ThreadLocal: Looper 通过 ThreadLocal 与线程绑定, 保证一个线程一个 Looper.

子线程 Looper 的创建与退出

子线程默认没有 Looper, 需先 Looper.prepare() 把 Looper 放入当前线程的 ThreadLocal, 创建绑定它的 Handler, 再 loop() 持续分发. 无参构造 Handler() 自 API 30 起已废弃, 应显式传 Handler(looper) 或 Handler(looper, callback). 退出用 quit() (立即丢弃未处理消息) 或 quitSafely() (处理已到期消息后退出); 不要创建没有退出策略的常驻 Looper. 工程上更常用 HandlerThread, 用法与生命周期见 多线程并发专题 - Android 线程模型与 HandlerThread.

// 示意代码: 生命周期 owner 应保存 handler, 在任务完成或销毁时发送 STOP.
Thread {
    Looper.prepare()
    val handler = Handler(Looper.myLooper()!!) { message ->
        when (message.what) {
            1 -> {
                refreshIndex(message.obj as String)
                true
            }
            2 -> {
                Looper.myLooper()?.quitSafely()
                true
            }
            else -> false
        }
    }
    handler.sendMessage(handler.obtainMessage(1, "books"))
    handler.sendEmptyMessage(2) // STOP: 处理已到期消息后退出
    Looper.loop()
}.start()

post 与 sendMessage 的职责

API队列载荷与分发适用场景错误选型后果
Handler.post(Runnable)Runnable 作为回调入队, 运行时直接执行该闭包一次性 UI 更新或明确任务用闭包携带复杂协议会难以取消, 分类和追踪
Handler.sendMessage(Message)Message.what 用于类别, obj/Bundle 携带数据, 最终到 handleMessage 或 Callback多类事件需要统一分发, 延迟和移除把大对象或 Activity 放入 obj 会延长引用生命周期并增加泄漏风险

二者都只是在目标 Looper 所在线程执行, 不自动切换到后台. Handler.post 是消息队列投递, 而 View.post 还依附于 View 的 attach 状态; 后者不等同于 “布局已完成”, 布局时机见 UI 体系 - View 与自定义 View.

UI 线程亲和性

View 层级, Window 和大多数 UI toolkit API 都由创建它们的主线程拥有. 子线程直接修改 View 会破坏遍历和事件分发的串行假设, 通常抛出 CalledFromWrongThreadException, 即使偶发未立即失败也属于竞态. 将结果投递回主线程只是最后一步; 若页面已经停止或 View 已销毁, 仍应丢弃过期渲染或通过生命周期感知的 state 收集处理.

为什么主线程死循环不卡死 / 不 ANR? loop () 是死循环, 但无消息时阻塞在 epoll_wait 让出 CPU, 有事件 (触摸, 绘制) 再唤醒. ANR 是消息处理太久, 不是循环本身.

进阶:

  • 同步屏障 (Sync Barrier): 插入屏障后, 同步消息被拦截, 优先处理异步消息 (如 UI 绘制 doFrame), 保证及时刷新.
  • IdleHandler: 队列空闲时回调, 可做延迟初始化 (不阻塞关键路径).

二, Binder 机制

Android 跨进程通信 (IPC) 的核心. 相比管道 / socket 的多次拷贝, Binder 通过 mmap 映射实现一次拷贝, 且内核自动附带调用方 UID/PID 便于安全鉴权. 结构上是 Client, Server, ServiceManager (服务注册查找), Binder 驱动; Client 拿到的是 BinderProxy, AIDL 自动生成 Stub/Proxy 代码.

一次拷贝简图

发送进程用户空间                 内核 / Binder 驱动              接收进程用户空间
Parcel 数据 -- 拷贝一次 --> binder_buffer -- mmap 同一页 --> Binder 线程读取 Parcel

发送方 Parcel 拷贝到驱动管理的 binder_buffer 是一次拷贝, 接收方经预先 mmap 的映射区直接读取, 省去一次显式拷贝. 它是 “一次拷贝, 不是零拷贝”, 大数据应走共享内存 / 文件描述符; 实现层 (mmap, binder_alloc, 线程池上限) 见 Binder 与 IPC 深入 - 一次拷贝模型与 mmap.

一次 Binder 调用怎么走

Client 的 Proxy 把参数写入 Parcel, 经驱动按 handle 路由到目标进程, 由 Binder 线程池回调 Stub 的 onTransact() 执行并回写 reply; 同步调用阻塞调用线程, oneway 不等待. 调用链见 Binder 与 IPC 深入 - AIDL 调用链, 线程池 / 死锁风险见 Binder 与 IPC 深入 - Binder 线程池与调用风险.

常见追问: 真零拷贝? 不是, 一次拷贝; 能传多大? 约 1MB 且并发共享, 见 TransactionTooLargeException 与 Parcelable 边界; 为何死锁? 同步调用持锁或主线程发 Binder 易锁等待 / ANR; AIDL 与 Binder? AIDL 是代码生成器, 底层仍走驱动. 完整问答见 Binder 与 IPC 深入 - 高频面试题.

三, 应用启动流程

点击图标到界面显示的大致链路: Launcher 经 Binder 请求 ATMS → 目标进程不存在时 Zygote fork 出应用进程, 执行 ActivityThread.main() 准备主线程 Looper → 进程经 Binder attachApplication, 系统回调创建 Application 与目标 Activity, 走 onCreate → onResume, ViewRootImpl 触发首帧. Zygote 是所有应用进程的 “母体”, 预加载核心类和资源, fork 时通过 COW (写时复制) 共享加速启动. 完整链路与源码细节见 Framework 源码与进程生命周期 - Activity 启动完整链路.

冷启动拆解与可优化点

阶段面试可讲的优化边界
进程创建 (Zygote fork)App 无法直接优化 fork 本身, 重点是减少 fork 后主线程工作.
ActivityThread.main()主线程消息循环不是卡顿来源, 卡顿来自消息处理超时.
bindApplication收敛 ContentProvider 自启动, 延迟 / 按需初始化 SDK.
Activity 首帧减少首屏布局层级和同步 IO, 用 Perfetto/Startup Timing 量化.

回答启动流程时要区分公开生命周期和系统实现细节: 对业务开发者稳定的是 Application/Activity 生命周期与进程可能被系统创建/杀死; AMS/ATMS, Zygote 命令格式, 预加载列表属于实现层面, 可作为理解但不宜写成永久不变的 API. 优化深度见 启动优化专项.

四, 类加载与热修复 / 插件化

  • 类加载器: PathClassLoader(加载已安装 APK), DexClassLoader(可加载外部 dex/jar, 热修复/插件化基础).它们持有 DexPathList, 内部是 dexElements 数组.
  • 热修复原理: 把修复后的补丁 dex 插到 dexElements 数组最前面, 根据双亲委派 + 数组顺序查找, 补丁类先被加载, 从而 “覆盖” 有 bug 的旧类 (如 Tinker, QFix). AndFix 则走 native 方法替换.
  • 插件化: 动态加载未安装的 APK, 需解决类加载, 资源加载 (AssetManager.addAssetPath), 组件生命周期 (Hook AMS / 占坑 Activity) 三大问题.

热修复方案对比

方案核心机制优点风险 / 边界
类加载补丁反射拿到宿主 ClassLoader 的 pathList.dexElements, 把补丁 dex 的 element 前插不直接改 ART 方法结构, 兼容性相对好; 适合 Java/Kotlin 逻辑修复已加载类通常不能被重新定义, 常需要冷启动生效; 受 multidex, 混淆, 校验和厂商改动影响.
native 方法替换修改 ART/Dalvik 方法入口或 native bridge, 让旧方法跳到新实现可做到即时生效的效果强依赖运行时内部结构, Android 版本/厂商 ROM 兼容风险高, 安全合规要求更高.
资源补丁构造新的 AssetManager/Resources, 通过 addAssetPath 或等价实现加入补丁资源包可修复图片, 布局, 字符串等资源资源 ID, 主题, 缓存对象可能已被解析; 高版本隐藏 API 限制和厂商差异需评估.

dexElements 前插流程可以按四步讲: 下载补丁并校验签名 / 版本 → 用 DexClassLoader 加载补丁 dex → 反射合并补丁与宿主的 dexElements → 重启进程或在安全时机让新类优先命中. 重点不是 “反射代码背诵”, 而是说明类查找顺序改变.

插件化三大难点

难点常见做法面试边界
类加载为插件创建独立 DexClassLoader, 或把插件 dex 合入宿主 ClassLoader独立加载隔离性好但共享类 / 类型转换要小心; 合并加载简单但冲突风险高.
资源加载为插件创建 Resources, 把插件 apk 路径加入 AssetManager, 再处理主题 / 资源 IDaddAssetPath 属实现层面方案, 隐藏 API/资源缓存/AssetLoader 演进会影响兼容.
组件生命周期manifest 未注册的 Activity/Service 不能直接被系统识别, 常用 “宿主占坑组件 + Hook Intent/Instrumentation/AMS 调度”Hook 系统调用链风险高, 受 Android 版本和厂商 ROM 影响; 线上要有降级与灰度.

占坑思路: 宿主 manifest 预注册一个透明 / 通用 Activity, 启动插件 Activity 前把 Intent 替换成占坑 Activity; 系统校验通过后, 在回调到应用侧时再把真实插件 Intent 换回来, 由插件框架接管生命周期与资源. 这个概念能说明 “为什么插件 Activity 不在 manifest 也能跑”, 但面试中要补一句: 这不是官方组件模型, 兼容性和合规风险需要框架兜底.

工程风险清单:

  • 补丁必须做签名, 版本, 灰度, 回滚, 不能把远端 dex 加载当成无约束能力.
  • 反射/隐藏 API 在高版本可能受限制, 厂商 ROM 对 ClassLoader/Resources 实现也可能不同.
  • native 替换与组件 Hook 改动运行时内部结构, 要用设备矩阵验证, 不要承诺 “所有版本通用”.
  • 热修复适合止血, 根因仍要通过正常发版修复; 插件化适合业务动态化, 不应绕过系统权限和安装模型.

Android 9 非 SDK 接口限制: 面试回答与工程边界

Android 9/API 28 起, 平台限制应用访问非 SDK 接口, 即未作为公开 Android SDK 契约提供的 framework 类, 方法和字段. 限制会随 Android 版本, targetSdk 与非 SDK 名单演进: 较低 targetSdk 在部分版本上可能仅收到警告或适用较宽的兼容行为, targetSdk 提升或名单收紧后则可能被阻止. 因此不能把某一设备或版本上反射成功当作长期兼容承诺.

面试若原题问 “如何绕过限制”, 应先纠正前提: 反射, 修改豁免名单或依赖第三方绕过都不是可维护方案, 也不应作为应用交付路径, 本章不提供规避代码. 先查找公开 SDK API, AndroidX 组件或有文档的系统服务契约; 无法替代时, 将最小依赖隔离在版本 / ROM 适配层, 做能力检测, 准备可用功能降级, 通过灰度和监控观察失败率, 并为移除该依赖制定版本化计划. 插件化和热修复的完整工程边界见 插件化, 热修复与动态化.

五, 进程间通信方式对比

方式特点场景
Binder一次拷贝, 安全, 面向对象Android 主流 IPC, AIDL
Socket通用, 两次拷贝, 慢跨设备, Zygote 通信
共享内存零拷贝, 最快, 需同步大数据 (匿名共享内存 Ashmem)
管道 / 消息队列传统 Linux IPC较少用
文件 / ContentProvider简单, 慢持久化数据共享

章节边界与源码版本

本章讲系统总览; 进程生命周期细节见 Framework 源码与进程生命周期, IPC 见 Binder 与 IPC 深入, 通用 OS 基础见操作系统与数据库基础. 描述 ActivityThread, AMS/ATMS, Zygote, LMKD 等内部实现时注明 Android 版本/AOSP tag; 内部类名和调用链不是稳定 SDK 契约.

高频面试题

Q1: 主线程 Looper 死循环为什么不会卡死 App? loop () 是死循环, 但 MessageQueue 无消息时通过 epoll 阻塞, 让出 CPU 不占用资源; 有消息 (触摸 / 绘制等) 再唤醒处理. ANR 是单条消息处理太久, 不是循环本身导致.

Q2: Handler 内存泄漏怎么产生? 如何避免? 非静态内部类 Handler 隐式持有外部 Activity, 若有延迟消息未处理, Activity 无法回收. 解决: 静态内部类 + 弱引用, 或在 onDestroy 中 removeCallbacksAndMessages (null).

Q3: Binder 为什么只需一次拷贝? 发送方 Parcel 拷贝到驱动 binder_buffer 一次, 接收方经 mmap 映射区读取, 省去一次显式拷贝; 它是 “一次拷贝, 不是零拷贝”, 且约 1MB 并发共享限制. 细节与边界见 Binder 与 IPC 深入.

Q4: 为什么用 Binder 而不是传统 IPC? 性能上一次拷贝优于管道/socket 的两次, 安全上内核自动附带调用方 UID/PID 支持鉴权. 详见 Binder 与 IPC 深入.

Q5: Zygote 的作用? 为什么用 fork? Zygote 预加载核心类库和资源, 作为所有应用进程的母体. fork 通过写时复制共享已加载内容, 加速进程创建, 减少内存占用 (避免每个进程重复加载 framework).

Q6: 热修复的基本原理? 利用类加载机制, 把补丁 dex 插入 ClassLoader 的 dexElements 数组前面, 使补丁类优先于有 bug 的旧类被加载. 属于 “类加载方案”, 通常需要重启或确保旧类尚未加载; 另有 native 方法替换方案, 但强依赖 ART 内部结构, 兼容风险更高.

Q7: ThreadLocal 在 Looper 里起什么作用? 保证一个线程只有一个 Looper. Looper.prepare 把 Looper 存入 ThreadLocal, 各线程独立互不干扰, getMainLooper 拿主线程的.

实践补充: 消息, 线程与启动时序

机制伪代码, 不能直接编译: Handler.post 把带 target 的 Message 入队; Looper.loop() 从 MessageQueue.next() 取消息 (空队列时 epoll 等待), 再经 target.dispatchMessage() 调用 Runnable 或 handleMessage(). 异步消息不等于后台线程, 主线程 Handler 的 Runnable 仍在主线程运行; 应用不应依赖隐藏 API 手工插同步屏障.

上下文片段 (放在 Activity): Activity 是该 HandlerThread 的生命周期 owner: 创建时启动, 销毁时调用 quitSafely(). Handler(worker.looper) 显式绑定 worker 的 Looper, 不使用已废弃的无参构造; 用法详见 多线程并发专题 - Android 线程模型与 HandlerThread.

private lateinit var worker: HandlerThread
override fun onCreate(state: Bundle?) { super.onCreate(state); worker = HandlerThread("thumbnail-worker").apply { start() }; Handler(worker.looper).post { loadThumbnailIndex() } }
override fun onDestroy() { worker.quitSafely(); super.onDestroy() }

预期是任务不占 UI 线程; 若 loadThumbnailIndex 长时间 I/O, 后续消息会排队. 症状是同一 worker 任务不执行, 证据是线程 dump 卡在 I/O; 修复为超时或有限并发执行器, 重复路径确认队列恢复.

Launcher -> ATMS/AMS (Binder) -> Zygote fork -> app ActivityThread.main
app -> AMS: attachApplication (Binder) -> ApplicationThread callback
-> ActivityThread main Handler -> 生命周期/首帧 -> SurfaceFlinger 合成

COW (写时复制) 是 fork 后先共享物理页, 写入时才复制该页的机制. 此图是 AOSP 心智模型, 具体类名和事务随版本变化. 启动测量见启动优化专项, AIDL 实现见 Binder 与 IPC 深入.