Android Framework 面试题:Binder都说不清楚,简历别写精通了
这是 Android 面试全景 专栏的第 1 篇。老规矩,Framework 这块是 Android 开发的天花板,也是面试官最爱挖的深水区。
一、Binder 为什么是 Android 的 IPC 首选?
核心回答
Binder 基于 C/S 架构,通过 mmap 实现一次数据拷贝,性能比 Socket/管道高一倍,同时在内核层校验进程 UID/PID,安全性也比传统 IPC 高出一截。
原理/代码
传统的 Socket/管道/消息队列,跨进程通信需要 两次数据拷贝:发送方从用户空间拷贝到内核缓冲区,接收方再从内核缓冲区拷贝到用户空间。而 Binder 借助 mmap 内存映射,在内核缓冲区和接收方用户空间之间建立直接映射,数据只需一次拷贝:
csharp
# 发送方
copy_from_user(data, kernel_buffer) // 1次拷贝
# 关键:Binder驱动建立映射关系
# 内核缓冲区 ←→ 接收方用户空间(共享同一块物理页)
# 接收方直接访问,无需再次拷贝
Binder 的架构设计也很有意思:
表格
| 角色 | 职责 |
|---|---|
| Client | 发起调用,持有 Binder 代理(Proxy) |
| Server | 响应请求,提供实际服务 |
| Binder驱动 | 内核模块,负责数据转发、引用计数 |
| ServiceManager | 域名服务,管理 Binder 名称到引用的映射 |
Android 实战场景
当你调用 getSystemService(WINDOW_SERVICE) 时,实际上是通过 Binder 拿到 WindowManager 的代理对象。这个代理对象把方法调用封装成 Parcel 数据包,交给 Binder 驱动转发给 system_server 进程中的 WindowManagerService 实际执行。
面试加分点
- Binder 的引用计数机制:每个 Binder 实体和引用都有引用计数,死亡通知(
linkToDeath/unlinkToDeath)可以监听服务端进程崩溃 - 线程池管理:默认最多 16 个 Binder 线程,通过
Process.setThreadPriority()可调整 - 实名 Binder vs 匿名 Binder:系统服务都是实名 Binder,通过 ServiceManager 注册;四大组件的 Binder 是匿名 Binder,通过 Intent 传递
- 极限数据量:mmap 默认 1M-8K,单次传输超过这个限制会触发异常
二、Binder 一次拷贝是怎么实现的?mmap 的原理是什么?
核心回答
mmap 把用户空间的一块虚拟内存和内核空间映射到同一块物理内存,数据从发送方到内核缓冲区后,接收方通过映射直接访问这块物理内存,无需再拷贝一次。
原理/代码
Linux 的虚拟内存寻址是理解 Binder 的基础:
scss
// Binder驱动的mmap实现(简化版)
// frameworks/native/libs/binder/ProcessState.cpp
// 进程初始化时调用
mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
// 关键:这个虚拟区域和内核缓冲区共享同一块物理页
// 发送方 copy_from_user() 到内核缓冲区
// 接收方直接通过虚拟地址访问这块物理内存
mmap 的三阶段原理:
- 建立映射 :进程调用
mmap()在虚拟地址空间创建空闲区域,内核建立页表映射关系 - 触发缺页:进程访问这块虚拟地址时,发现物理页不存在,触发缺页异常
- 加载数据:内核将数据从磁盘/文件读取到物理页,建立完整的页表映射
Binder 在内核空间创建接收缓存区,通过 remap_pfn_range 把这块内核缓冲区和接收进程的用户空间映射到同一物理页。
Android 实战场景
AIDL 生成的代码中,Proxy 端通过 Parcel 打包数据,然后调用 IBinder.transact():
ini
// AIDL生成的Proxy
public void getUserName(int userId, Callback callback) {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
// 写入方法码和参数
data.writeInt(userId);
// 跨进程调用,数据经过Binder驱动mmap传输
mRemote.transact(TRANSACTION_getUserName, data, reply, 0);
// 读取返回结果
int result = reply.readInt();
callback.onResult(result);
}
面试加分点
- Binder 驱动创建的是虚拟接收缓冲区 ,通过
copy_from_user()把发送方数据写入这个缓冲区,再通过映射让接收方直接访问 - mmap 的
MAP_PRIVATE标志意味着 Copy-On-Write:接收方写入时才会复制物理页 - Linux 的
epoll机制也是 mmap 应用:epoll 的红黑树和就绪链表都通过共享内存实现,高效监听文件描述符事件 - 为什么共享内存是 0 次拷贝但 Binder 不选它?共享内存控制复杂,容易出现死锁,稳定性差
三、Handler 的底层原理:MessageQueue 为什么不是用 List 实现的?epoll 机制是什么?
核心回答
MessageQueue 用单向链表而不是数组/列表,是因为消息需要按执行时间排序插入,链表的插入复杂度是 O(1),而数组最差是 O(n);epoll 让主线程在没有消息时进入休眠,不消耗 CPU。
原理/代码
MessageQueue 的核心是按时间排序的单向链表:
ini
// MessageQueue.java
Message mMessages; // 链表头,按 when 时间从小到大排序
boolean enqueueMessage(Message msg, long when) {
Message prev = null;
Message p = mMessages;
if (p == null || when < p.when) {
// 插入到链表头部 - O(1)
msg.next = p;
mMessages = msg;
} else {
// 遍历找到合适位置 - O(n)
while (p.next != null && p.next.when <= when) {
prev = p;
p = p.next;
}
prev.next = msg;
msg.next = p.next;
}
return true;
}
为什么不用数组?假设用 ArrayList<Message>,插入一条延迟消息时,最坏情况(插入到列表开头)需要移动后面所有元素,复杂度 O(n)。链表只需要修改指针,插入到末尾也是 O(1)。
epoll 的作用在于 nativePollOnce:
csharp
// MessageQueue.next()
Message next() {
for (;;) {
// 关键:阻塞等待,直到有消息或超时
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
Message msg = mMessages;
if (msg != null && msg.when <= now) {
return msg; // 有消息,取出
}
// 没有消息,设置超时继续阻塞
nextPollTimeoutMillis = msg.when - now;
}
}
}
底层调用 Linux 的 epoll_wait,没有事件时线程进入 TASK_INTERRUPTIBLE 状态,不占用 CPU。
Android 实战场景
一个常见的误区是以为 postDelayed 会开启定时器轮询。实际上,系统只是把 "当前时间 + 延迟时长" 记作执行时间 when,消息按时间顺序插入链表,Looper 取消息时如果时间未到就继续阻塞:
scss
// 错误的理解
mHandler.postDelayed(runnable, 1000);
// 系统:开启一个1秒的定时器...
// 正确的理解
mHandler.postDelayed(runnable, 1000);
// 系统:计算 when = now + 1000,插入MessageQueue链表
// Looper循环时发现时间未到,epoll_wait阻塞1秒后唤醒
面试加分点
- 同步屏障(SyncBarrier):通过
postSyncBarrier()插入一个特殊消息,Looper 会跳过同步消息优先处理异步消息,用于保证 UI 绘制的优先级 - IdleHandler:MessageQueue 空闲时的回调,可用于性能优化(如 Android 系统用它做预创建 View)
- 消息分发优先级:
msg.callback.run()→Handler.mCallback→Handler.handleMessage() - 唤醒机制:发消息时通过管道写操作唤醒 epoll_wait,不仅仅是延时倒计时
四、Handler 导致的内存泄漏,根本原因是什么?怎么彻底解决?
核心回答
内存泄漏的本质是长生命周期对象持有短生命周期对象的引用:MessageQueue 持有 Message,Message.target 持有 Handler,Handler(非静态内部类)持有 Activity,形成了 GC Root 到 Activity 的引用链。
原理/代码
典型的泄漏场景:
scala
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 非静态内部类:编译器自动注入 MainActivity.this
// 这行代码隐式持有外部Activity引用
updateUI();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 发送一个10秒后的延迟消息
mHandler.sendEmptyMessageDelayed(0, 10000);
// 如果5秒后用户按返回键...
finish(); // Activity无法被GC回收!因为MessageQueue里还有消息
}
}
引用链:Looper (应用生命周期) → MessageQueue → Message → Handler → Activity
泄漏的触发条件:
- Handler 是非静态内部类(持有外部Activity引用)
- MessageQueue 中有未处理的消息(特别是延迟消息)
- 页面已销毁但消息还在排队
Android 实战场景
实际项目中,泄漏往往发生在网络请求回调、动画等场景:
scala
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler(Looper.getMainLooper());
private Runnable mAnimRunnable = new Runnable() {
@Override
public void run() {
// 动画循环
mImageView.animate().translationX(100).setDuration(1000)
.withEndAction(this); // 循环引用Activity
}
};
@Override
protected void onDestroy() {
// 没有移除动画回调,Activity泄漏
// mImageView.animate() 持有 View,View 持有 Activity
super.onDestroy();
}
}
彻底解决方案
方案一:静态内部类 + 弱引用(推荐)
scala
public class MainActivity extends AppCompatActivity {
// 静态内部类不持有外部类引用
private static class SafeHandler extends Handler {
private final WeakReference<MainActivity> mActivityRef;
SafeHandler(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivityRef.get();
if (activity == null || activity.isFinishing()) {
return; // Activity已销毁,不处理
}
activity.updateUI();
}
}
private SafeHandler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new SafeHandler(this);
}
@Override
protected void onDestroy() {
// 移除所有消息和回调(关键!)
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
方案二:配合 Lifecycle 自动解绑
scala
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
}
});
}
}
面试加分点
removeCallbacksAndMessages(null)会移除所有该 Handler 的消息,不传参数只移除指定 Runnable- Message 本身也有泄漏风险:如果 Message 被长期持有(如静态变量),它的
target字段会阻止 Handler 被 GC - 使用 Jetpack 的
LiveData或协程可以更优雅地替代 Handler 处理生命周期相关逻辑 - 检测工具:LeakCanary、MAT、Android Studio Profiler
五、AMS 是什么?一个 Activity 的启动流程是怎样的?
核心回答
AMS(ActivityManagerService)是 Android 最核心的系统服务之一,运行在 system_server 进程,负责管理四大组件的生命周期、进程优先级和任务栈。Activity 启动涉及 发起方进程 → AMS → Zygote → 应用进程 的跨进程协作。
原理/代码
Activity 启动流程的核心链路:
scss
Launcher.startActivity()
↓ (Binder调用)
ActivityManagerService.startActivity()
↓
ActivityStackSupervisor.resolveActivity() // 解析Intent,确定目标Activity
↓
ActivityStackSupervisor.startActivityMayWait()
↓
ActivityStack.startActivityLocked()
↓
ActivityStack.startActivityUncheckedLocked()
↓
ActivityStackSupervisor.startActivityLocked() // 处理栈管理、LaunchMode
↓
ActivityStack.resumeTopActivityInnerLocked()
↓
if (进程不存在) {
// 冷启动:创建新进程
Process.start("android.app.ActivityThread")
↓
Zygote.fork() → 应用进程
↓
ActivityThread.main()
↓
Looper.prepareMainLooper() + Looper.loop()
} else {
// 热启动:进程已存在
ApplicationThread.scheduleLaunchActivity()
}
↓
ActivityThread.handleLaunchActivity()
↓
Instrumentation.newActivity() // 创建Activity实例
↓
Instrumentation.callActivityOnCreate()
↓
Activity.onCreate()
↓
ActivityThread.handleResumeActivity()
↓
WindowManager.addView() // 与WMS交互,添加窗口
关键类的作用:
表格
| 类 | 位置 | 职责 |
|---|---|---|
| AMS | system_server | 组件生命周期管理、进程调度 |
| ActivityStack | AMS进程 | 管理Activity栈、维护返回栈 |
| ActivityStackSupervisor | AMS进程 | 栈的具体操作、LaunchMode处理 |
| ApplicationThread | 应用进程 | Binder服务端,接收AMS指令 |
| Instrumentation | 应用进程 | 组件创建、生命周期调用 |
Android 实战场景
当你从 Launcher 点击图标启动 App 时,实际经历的是冷启动(进程不存在):
- AMS 检查目标进程是否存在
- 不存在,通过 Socket 向 Zygote 发送 fork 请求
- Zygote fork 出新进程,调用新进程的
ActivityThread.main() - 新进程初始化 Application,通过 Binder 通知 AMS "我启动了"
- AMS 发送
scheduleLaunchActivity指令 - 应用进程收到指令,创建 Activity 并执行生命周期
面试加分点
- LaunchMode 的实现原理 :ActivityStack 维护 TaskRecord,通过
taskAffinity、intent flags(FLAG_ACTIVITY_NEW_TASK 等)控制任务栈行为 - 进程优先级:Android 根据组件运行状态(前台进程、可见进程、服务进程等)动态调整进程优先级,OOM_adj 值越低优先级越高
- Activity 状态保存 :
onSaveInstanceState在 Activity 进入 STOPPED 状态前调用,数据存储在 ActivityRecord 的 Bundle 中 - 多 Intent 叠加:如果同一个 Activity 被连续启动多次,返回时会看到多个实例或单一实例取决于 LaunchMode
六、WMS 是什么?View 的绘制和 WMS 的关系?
核心回答
WMS(WindowManagerService)管理所有窗口的 Z 轴顺序、位置、尺寸和生命周期。View 的绘制通过 ViewRootImpl 连接 View 树和 WMS,最终由 SurfaceFlinger 合成到屏幕。
原理/代码
Window 的三层架构:
scss
PhoneWindow (Activity持有)
↓ 管理
DecorView (根布局)
↓ 添加到
ViewRootImpl (每个Window对应一个)
↓ 通过Binder调用
WindowManagerService (system_server进程)
↓ 管理
WindowState (WMS内部数据结构)
↓
SurfaceFlinger (硬件渲染服务)
View 绘制流程:
scss
// ViewRootImpl.setView()
public void setView(View view, WindowManager.LayoutParams attrs,
View panelParentView) {
requestLayout(); // 触发测量、布局、绘制
// 通过Binder调用WMS添加窗口
res = mWindowSession.addToDisplay(mWindow, ...);
}
// ViewRootImpl.performTraversals() - 绘制三连
private void performTraversals() {
// 1. measure - 计算大小
measureHierarchy(root, lp, Resizer够了, desiredBitmap);
// 2. layout - 计算位置
layout(widthMeasureSpec, heightMeasureSpec);
// 3. draw - 绘制到Surface
draw(fullRedrawNeeded);
}
关键点:View 绘制的数据最终写入 Surface,Surface 是一块共享内存,由 SurfaceFlinger 读取并合成显示。
Android 实战场景
Dialog 或 PopupWindow 的显示原理:
scss
// Dialog.show() 的本质
Dialog.show() {
// 1. 创建PhoneWindow
mWindow = new PhoneWindow(context);
// 2. 添加DecorView到WMS
mWindowManager.addView(mDecor, lp);
// 3. WMS创建WindowState,分配Z轴顺序
// Dialog默认比Activity的窗口Z轴高,所以能覆盖Activity
}
Toast 的特殊之处:它使用的是系统窗口,权限比普通应用高,所以即使 App 被切到后台也能显示。
面试加分点
- Window 类型:应用窗口(TYPE_APPLICATION)、子窗口(TYPE_APPLICATION_PANEL)、系统窗口(TYPE_SYSTEM_ALERT)。不同类型有不同的 Z 轴层级和权限要求
- Surface 的双缓冲:前端 Surface 用于显示,后端 Surface 用于绘制,double buffering 避免闪烁
- IMS(InputManagerService) 和 WMS 配合:触摸事件先到 IMS,通过 WMS 找到当前焦点窗口,分发给对应的 View
- 虚拟显示:Android 10+ 支持多屏幕/折叠屏,WMS 需要管理多个 Display,每个 Display 有独立的窗口栈
七、Zygote 是什么?App 启动流程中 Zygote 做了什么?为什么用 fork 而不是新建进程?
核心回答
Zygote 是 Android 的 "进程孵化器",它预先加载 ART 虚拟机和所有 Framework 类,fork 时通过 Copy-On-Write 机制让子进程直接继承这些资源,比每次新建进程再初始化快 85%。
原理/代码
Zygote 启动时做三件事:
- 创建 ART 虚拟机:加载 libart.so,创建 Zygote Heap
- 预加载系统资源:加载 ~6000 个常用类、字体、资源表、共享库
- 创建 Socket 监听:等待 AMS 发送 fork 请求
scss
// ZygoteInit.java 简化流程
public static void main(String[] args) {
// 1. 创建虚拟机
runtime = new AndroidRuntime();
runtime.start("com.android.internal.os.ZygoteInit", args, startSystemServer);
// 2. 预加载(耗时约3秒)
preload(); // 加载类、资源、库
// 3. 创建Socket,等待fork请求
zygoteServer.registerServerSocket(socketName);
// 4. 进入循环
runSelectLoop();
}
fork 的核心优势:Copy-On-Write(写时复制)
scss
// fork后,父子进程共享同一份物理内存(只读)
// 只有当某个进程试图写入时,才会复制物理页
// Zygote预加载的内容(代码段、Framework类)都是只读的
// 所以fork开销极低:只复制页表,不复制实际内存
// 新进程fork后
pid = fork();
if (pid == 0) {
// 子进程:加载应用代码,跳过虚拟机初始化
// 因为已经继承了Zygote的ART虚拟机
ZygoteInit.childZygoteInit(args);
}
Android 实战场景
App 冷启动耗时对比:
表格
| 方式 | 耗时 | 原因 |
|---|---|---|
| 传统方式(每次初始化虚拟机) | ~2000ms | 创建新进程 + 初始化 ART + 加载 Framework |
| Zygote fork | ~300ms | 直接继承预加载资源,只需加载应用代码 |
| 提升 | 85% | COW 机制 |
Zygote 不用 Binder 通信的原因:
- fork + 多线程 = 死锁风险:假设 fork 前子线程持有一个锁,fork 后子进程中没有这个线程,锁没有主人,永远无法释放
- Zygote 在 fork 前会
VMThreads.getInstance().stop()暂停所有线程,fork 后再恢复 - Socket 更轻量:只需要传递几个字符串参数,不需要 Binder 的序列化/反序列化开销
面试加分点
- 双 Zygote 架构 :Android 8+ 支持
zygote32和zygote64,分别服务 32 位和 64 位应用,减少内存碎片 - USAP(Unspecialized App Process) :Android 12+ 引入的优化,预创建空白进程,fork 时不需要等待
- SystemServer:第一个从 Zygote fork 出的子进程,运行 100+ 系统服务(AMS/WMS/PMS 等),启动时间约 5-10 秒
- Zygote 预加载优化 :Android 15 支持延迟预加载(
--enable-lazy-preload),非热路径类按需加载
八、Android 的类加载机制:PathClassLoader 和 DexClassLoader 的区别?热修复原理?
核心回答
PathClassLoader 用于加载已安装应用的 DEX,只能加载 data/dalvik-cache 下的优化文件;DexClassLoader 多一个 optimizedDirectory 参数,可以从任意路径加载 DEX,所以是热修复和插件化的基础。
原理/代码
两者的继承关系:
scala
// PathClassLoader - 加载已安装应用的DEX
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
// optimizedDirectory = null,使用默认路径 /data/dalvik-cache
}
}
// DexClassLoader - 加载任意路径的DEX
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
// optimizedDirectory 自定义odex存放目录
}
}
// 实际加载逻辑在 BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) {
// 遍历 dexElements 数组
Class c = pathList.findClass(name);
return c;
}
}
DexPathList 的结构:
kotlin
// DexPathList.java
class DexPathList {
private Element[] dexElements; // 关键:DEX文件数组
// findClass 遍历查找
public Class findClass(String name) {
for (Element element : dexElements) {
Class clazz = element.findClass(name, this);
if (clazz != null) {
return clazz; // 找到就返回
}
}
return null; // 未找到
}
}
热修复原理
基于 DexElements 优先级 和 双亲委托模型:
ini
// 1. 修复后的DEX(补丁包)插入 dexElements 数组最前面
// 2. 加载类时,优先从补丁DEX中找到修复后的类
// 伪代码
String patchDexPath = "/data/data/com.app/patch.dex";
DexClassLoader patchLoader = new DexClassLoader(patchDexPath, ...);
// 获取 PathClassLoader 的 dexElements
PathClassLoader appLoader = (PathClassLoader) context.getClassLoader();
Object pathList = getPathList(appLoader);
Object appDexElements = getDexElements(pathList);
// 获取补丁 DexClassLoader 的 dexElements
Object patchDexElements = getDexElements(getPathList(patchLoader));
// 合并:补丁dexElements 放在数组前面
Object[] newDexElements = combine(patchDexElements, appDexElements);
// 反射设置回 PathClassLoader
setDexElements(pathList, newDexElements);
Android 实战场景
主流热修复方案对比:
表格
| 方案 | 代表 | 原理 | 特点 |
|---|---|---|---|
| Dex插桩 | QZone | 插桩触发类加载,手动合并dexElements | 需要编译时插桩 |
| 全量替换 | Tinker | 生成差异包,合并后替换BaseDex | 补丁小,但需要重启 |
| Native替换 | AndFix/Alibaba | 直接替换ArtMethod结构 | 粒度细,但兼容性问题 |
面试加分点
- 双亲委托模型:ClassLoader 加载类时先委托父加载器,只有父加载器找不到才自己加载。好处是避免类的重复加载(BootClassLoader → ExtClassLoader → PathClassLoader)
- 热部署 vs 冷部署:只改方法体(热部署)可以不需要重启;改了类结构(增删方法/字段)需要重启(JIT 除外)
- 资源热修复 :需要创建新的 AssetManager,先加载补丁资源包再加载原包:
assetManager.addAssetPath(patchApk); - SO 库热修复 :通过反射把补丁 SO 的路径插入
nativeLibraryDirectories数组前面
九、SharedPreferences 的 ANR 问题:为什么 commit 和 apply 都可能 ANR?
核心回答
commit 在主线程同步写文件,写入耗时长就会卡住主线程;apply 虽然异步,但系统会在 Activity/Service 生命周期节点(onStop、onPause 等)调用 QueuedWork.waitToFinish() 等待所有 apply 完成,等待期间如果文件还没写完就会阻塞主线程。
原理/代码
scss
// commit:主线程同步等待
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
// 同步写入,会阻塞等待
enqueueDiskWrite(mcr, null); // 第二个参数null表示同步
mcr.writtenToDiskLatch.await(); // 阻塞直到写完
return mcr.writeToDiskResult;
}
// apply:异步写入
public void apply() {
MemoryCommitResult mcr = commitToMemory();
// 创建等待任务
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await(); // 等待写入完成
}
};
// 添加到 QueuedWork 的 finisher 队列
QueuedWork.addFinisher(awaitCommit);
// 异步执行写入
enqueueDiskWrite(mcr, postWriteRunnable);
}
QueuedWork.waitToFinish() 的调用时机:
java
// ActivityThread.java
@Override
public void handleStopActivity(IBinder token, boolean show, int configChanges,
PendingTransactionAction pendingActions) {
// ...
// 在这里等待所有 apply 完成
QueuedWork.waitToFinish();
// 如果apply的文件写入还没完成,主线程会一直等
}
// 同样在 handleStopService、handlePause 等生命周期调用
Android 实战场景
典型 ANR 场景:
typescript
// 用户快速切换页面
public class MainActivity extends AppCompatActivity {
private SharedPreferences sp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sp = getSharedPreferences("config", MODE_PRIVATE);
}
public void saveData(View view) {
// 连续10次apply
for (int i = 0; i < 10; i++) {
sp.edit().putString("key_" + i, "value_" + i).apply();
}
}
@Override
protected void onStop() {
super.onStop();
// 触发 waitToFinish(),如果写入没完成会阻塞
}
}
解决方案
- 用 commit 代替 apply(需要返回值时) :
commit在子线程执行写操作 - 不要在
apply后立即finish():给系统留出写入时间 - 拆分文件:每个 SP 文件不要太大,避免加载和写入耗时
- 使用替代方案:MMKV(微信开源)、Jetpack DataStore
面试加分点
apply在 Android 8.0 之前更容易 ANR:waitToFinish会直接在主线程执行写入任务,8.0 后改为先执行异步任务再等待- SP 不支持多进程:多进程写入会导致数据丢失或覆盖,跨进程场景用 ContentProvider 或 MMKV
- 文件写入的原子性 :写入前会先创建
.bak备份文件,写入成功后再删除备份,保证数据安全 - 内存缓存 :
getXXX()方法会阻塞等待文件加载完成,首次访问 SP(进程内第一次)会比较慢
十、ContentProvider 的启动时机和生命周期?多进程访问时的并发问题?
核心回答
ContentProvider 在应用启动时(Application.onCreate() 之前)就完成了初始化,通过 installContentProviders() 创建实例并调用 onCreate()。多进程访问时,每个进程都有独立的 Provider 实例,但底层数据是共享的,需要自己做并发控制。
原理/代码
Provider 的启动时机:
scss
// ActivityThread.handleBindApplication()
private void handleBindApplication(AppBindData data) {
// 1. 创建 Application
mInstrumentation.callApplicationOnCreate(app);
// 2. 安装 ContentProvider(Application.onCreate之前!)
List<ProviderInfo> providers = data.providers;
if (providers != null) {
installContentProviders(app, providers);
}
}
// installContentProviders
private void installContentProviders(Context context, List<ProviderInfo> providers) {
for (ProviderInfo cpi : providers) {
// 创建Provider实例
ContentProvider localProvider = createProviderContext(cpi);
// 反射创建Provider
localProvider = (ContentProvider) cl.loadClass(info.name).newInstance();
// 关键:调用 onCreate()
localProvider.attachInfo(context, info);
// 通知AMS,Provider已经publish
ActivityManager.getService().publishContentProviders(...);
}
}
Provider 的生命周期:
scss
Provider.onCreate() → 应用启动时调用,初始化资源
↓
首次被访问(query/insert/update/delete)
↓
可能有多个线程同时调用
↓
onShutdown() → 所有Client断开连接时调用
Android 实战场景
多进程并发问题:
java
public class UserProvider extends ContentProvider {
private SQLiteDatabase db;
@Override
public boolean onCreate() {
db = new DBHelper(getContext()).getWritableDatabase();
return true;
}
@Override
public synchronized Cursor query(Uri uri, ...) {
// 问题1:多个进程同时调用,每个进程有自己的 db 实例
// 但SQLiteDatabase本身是线程安全的(锁机制)
return db.query("user", ...);
}
@Override
public synchronized Uri insert(Uri uri, ContentValues values) {
// 问题2:如果业务层有更复杂的并发逻辑
// 比如先查询再插入,需要事务来保证原子性
db.beginTransaction();
try {
// 业务逻辑
db.insert("user", null, values);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return uri;
}
}
并发问题解决方案
- SQLiteDatabase 自身线程安全:单个数据库实例的读写操作是串行化的
- 使用事务 :批量操作使用
beginTransaction()+setTransactionSuccessful()+endTransaction() - ContentProviderOperation 批量操作:
less
ContentResolver resolver = getContentResolver();
ArrayList<ContentProviderOperation> operations = new ArrayList<>();
operations.add(ContentProviderOperation.newInsert(uri1)
.withValue("name", "Alice").build());
operations.add(ContentProviderOperation.newUpdate(uri2)
.withValue("age", 25)
.withSelection("name = ?", new String[]{"Bob"}).build());
// 批量执行,要么全部成功,要么全部回滚
resolver.applyBatch(AUTHORITY, operations);
面试加分点
- URI 匹配 :
UriMatcher用于解析 Provider 的 URI path,匹配码决定是调用哪个方法 - ContentObserver 监听数据变化 :注册 Observer 后,数据变化会触发
onChange()回调 - AMS 的进程控制 :当 ContentProvider 所在进程被 kill 时,所有持有该 Provider 引用的进程会被断开连接;可通过
stable和unstable引用控制是否跟随进程销毁 - 启动延迟优化 :如果 Provider 初始化很耗时,可以考虑懒加载(首次访问时再初始化),但要注意权限校验可能在
attachInfo时已经执行
总结
Framework 这块没有捷径,只能靠理解 + 源码。建议的学习路径:
- Binder → 理解跨进程通信的本质
- Handler → 理解线程间通信和消息机制
- AMS/WMS → 理解四大组件管理
- Zygote → 理解进程创建和资源复用
- ClassLoader → 理解动态加载和热修复