Android Framework 面试题:Binder都说不清楚,简历别写精通了

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 的三阶段原理:

  1. 建立映射 :进程调用 mmap() 在虚拟地址空间创建空闲区域,内核建立页表映射关系
  2. 触发缺页:进程访问这块虚拟地址时,发现物理页不存在,触发缺页异常
  3. 加载数据:内核将数据从磁盘/文件读取到物理页,建立完整的页表映射

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.mCallbackHandler.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

泄漏的触发条件:

  1. Handler 是非静态内部类(持有外部Activity引用)
  2. MessageQueue 中有未处理的消息(特别是延迟消息)
  3. 页面已销毁但消息还在排队

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 时,实际经历的是冷启动(进程不存在):

  1. AMS 检查目标进程是否存在
  2. 不存在,通过 Socket 向 Zygote 发送 fork 请求
  3. Zygote fork 出新进程,调用新进程的 ActivityThread.main()
  4. 新进程初始化 Application,通过 Binder 通知 AMS "我启动了"
  5. AMS 发送 scheduleLaunchActivity 指令
  6. 应用进程收到指令,创建 Activity 并执行生命周期

面试加分点

  • LaunchMode 的实现原理 :ActivityStack 维护 TaskRecord,通过 taskAffinityintent 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 启动时做三件事:

  1. 创建 ART 虚拟机:加载 libart.so,创建 Zygote Heap
  2. 预加载系统资源:加载 ~6000 个常用类、字体、资源表、共享库
  3. 创建 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+ 支持 zygote32zygote64,分别服务 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 生命周期节点(onStoponPause 等)调用 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(),如果写入没完成会阻塞
    }
}

解决方案

  1. 用 commit 代替 apply(需要返回值时)commit 在子线程执行写操作
  2. 不要在 apply 后立即 finish() :给系统留出写入时间
  3. 拆分文件:每个 SP 文件不要太大,避免加载和写入耗时
  4. 使用替代方案: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;
    }
}

并发问题解决方案

  1. SQLiteDatabase 自身线程安全:单个数据库实例的读写操作是串行化的
  2. 使用事务 :批量操作使用 beginTransaction() + setTransactionSuccessful() + endTransaction()
  3. 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 引用的进程会被断开连接;可通过 stableunstable 引用控制是否跟随进程销毁
  • 启动延迟优化 :如果 Provider 初始化很耗时,可以考虑懒加载(首次访问时再初始化),但要注意权限校验可能在 attachInfo 时已经执行

总结

Framework 这块没有捷径,只能靠理解 + 源码。建议的学习路径:

  1. Binder → 理解跨进程通信的本质
  2. Handler → 理解线程间通信和消息机制
  3. AMS/WMS → 理解四大组件管理
  4. Zygote → 理解进程创建和资源复用
  5. ClassLoader → 理解动态加载和热修复
相关推荐
萌新杰少2 小时前
安卓原生项目迁移KMP——核心迁移
android·kotlin·jetbrains
小孔龙2 小时前
AndroidManifest.xml 配置速查手册
android
Gauss松鼠会2 小时前
【GaussDB】基于SpringBoot实现操作GaussDB(DWS)的项目实战
java·数据库·经验分享·spring boot·后端·sql·gaussdb
Gauss松鼠会2 小时前
【GaussDB】GaussDB 常见问题及解决方案汇总
java·数据库·算法·性能优化·gaussdb·经验总结
xiaogg36782 小时前
k8s 部署yaml文件和Dockerfile文件配置
java·docker·kubernetes
七牛云行业应用2 小时前
OpenAI Codex手机版上线实战:iOS/Android 5步配置远程控制指南(2026)
android·ios·智能手机
砍材农夫2 小时前
物联网 基于netty构建mqtt协议规范(发布/订阅模式)
java·开发语言·物联网·netty
techdashen2 小时前
Rust 泛型 vs Java 泛型:它们看起来相似,但骨子里截然不同
java·开发语言·rust
背包客(wyq)2 小时前
YOLO手势检测识别模型Android端部署测试
android·yolo