# AppStateManager **Repository Path**: applematrix/app-state-manager ## Basic Information - **Project Name**: AppStateManager - **Description**: 跨进程共享内存快速同步数据的工具工程,不使用binder - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-07 - **Last Updated**: 2026-07-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # AppStateManager 基于 Android Ashmem 共享内存的跨进程状态管理方案,实现低延迟数据读取与变更通知。 --- ## 目录 1. [设计背景与问题场景](#1-设计背景与问题场景) 2. [候选方案对比与选择](#2-候选方案对比与选择) 3. [关键设计细节](#3-关键设计细节) 4. [性能设计指标](#4-性能设计指标) 5. [安全设计](#5-安全设计) 6. [架构与内存布局](#6-架构与内存布局) 7. [项目结构](#7-项目结构) 8. [测试用例](#8-测试用例) 9. [构建与运行](#9-构建与运行) --- ## 1. 设计背景与问题场景 ### 要解决的问题 在 Android 系统中,**应用冻结(App Freezer)** 是系统内存管理的关键机制:当应用进入后台且不再需要执行时,系统冻结进程以回收 CPU 资源、节省功耗。多个系统组件(ActivityManager、BatteryStats、ThermalManager 等)需要**实时查询某个进程是否处于冻结状态**,以便做出调度决策。OEM厂商在系统中集成自研冻结组件,也需要感知应用的冻结状态,否则可能导致重复冻结或者冻结冲突的问题。 核心矛盾在于:冻结状态的管理组件可能运行在系统进程(如 `system_server`),而冻结状态的查询组件可能由于权限隔离、进程沙箱等原因运行在独立进程。如果要想感知系统中应用的冻结状态,必须跨进程查询。传统 Binder IPC 查询存在以下问题: - **时延高**:单次 Binder 往返 ~96μs,高频查询时成为瓶颈 - **CPU 开销大**:每次 IPC 涉及 Binder 驱动、线程唤醒、数据拷贝 - **不可扩展**:查询方增多时,IPC 次数线性增长,系统负载恶化 - **负载随机**:系统中的IPC线程资源有限,可能会并发而非常繁忙,如果无法分配空闲线程来响应查询请求,可能导致阻塞等待 ### 应用场景 | 场景 | 描述 | |------|------| | **进程冻结状态查询** | 系统组件需要高频查询某个进程是否被冻结,以决定是否向其发送广播、调度任务等 | | **电池与性能监控** | 功耗管理模块需要实时感知后台进程的冻结/解冻变化 | | **进程生命周期追踪** | 监听进程的创建、冻结、解冻、死亡事件,驱动上层策略逻辑 | | **跨进程数据一致性** | 多个消费者进程需要看到同一份权威数据,且不能有读不一致问题 | | **跨进程数据低延时响应** | 系统中应用的冻结状态需要能够快速查询到,如果查询到过期数据,可能导致控制紊乱 | --- ## 2. 候选方案对比与选择 在设计初期,我们评估了以下四种跨进程数据共享方案: ### 方案对比 | 方案 | 读取时延 | 写入方 | 数据一致性 | 变更通知 | 安全性 | 实现复杂度 | |------|---------|--------|-----------|---------|--------|-----------| | **A. 纯 Binder IPC** | ~96μs | Owner 独占 | 强(单点) | 无需(每次查询) | 高 | 低 | | **B. Socket / Unix Domain Socket** | ~80-120μs | 双方均可 | 弱(无锁) | 需自建 | 中 | 中 | | **C. tmpfs mmap + 文件锁** | ~5-10μs | 双方均可 | 中(文件锁粒度粗) | 需自建 | 低(任意写入) | 中 | | **D. Ashmem + pthread 同步** ✅ | **~7μs** | Owner 独占 | 强(跨进程互斥锁) | 原生(条件变量) | 高(mprotect + 内核管理) | 中高 | ### 选择方案 D 的原因 1. **时延优势**:本地 mmap 读取 ~7μs,比 Binder IPC 快 **~13.5x**,满足高频查询需求 2. **原生同步原语**:`pthread_mutex_t` / `pthread_cond_t` 设置 `PTHREAD_PROCESS_SHARED` 后天然支持跨进程,无需自建同步协议 3. **内核级安全保障**:Ashmem 由 Android 内核管理,进程死亡时 fd 自动回收;`mprotect(PROT_READ)` 强制 Client 端数据区只读,防止篡改 4. **Binder 集成友好**:Ashmem fd 可通过 Binder `ParcelFileDescriptor` 跨进程传递,与现有 AIDL 体系无缝集成 5. **事件驱动通知**:`pthread_cond_broadcast` 实现 push 模式变更通知,Client 无需轮询,省 CPU ### 其他方案的局限 - **方案 A**:时延高,无法满足高频查询(系统级场景可能每秒数千次查询),而且受 Binder 线程资源限制,可能导致系统高负载时延时剧增 - **方案 B**:Socket 涉及多次数据拷贝和系统调用,时延无优势;缺乏内置同步机制 - **方案 C**:tmpfs 上任意 mmap 方均可写入,缺乏权限隔离;文件锁无法实现细粒度的读写分离;文件在进程异常退出时可能残留 --- ## 3. 关键设计细节 ### 3.1 Owner-Client 角色分离 | 角色 | 进程 | 权限 | 职责 | |------|------|------|------| | **Owner** | `SharedMemoryService` (`:service`),模拟系统进程 | 读写 | 创建 Ashmem,初始化跨进程锁,所有数据写入 | | **Client** | `MainActivity` (主进程) ,模拟应用进程 | 数据区只读,锁区可读写 | 通过 Binder 获取 fd,mmap 后本地读取,等待条件变量通知 | **Owner 写入控制的设计逻辑** - 数据的拥有者具有所有数据的写入权限,非owner主要是查询行为 - 单一写入源保证数据一致性,避免多写者冲突 - 系统层进行管控,配合 `mprotect(PROT_READ)` 提供硬件级防篡改,防止客户端恶意攻击或bug漏洞导致数据错误。 - Client 端通过 Binder IPC 请求 Owner 执行写操作,写路径虽走 Binder,但读路径走本地共享内存,实现读写分离优化,client需要更新数据的概率较低,因此读路径可以走本地共享内存,写操作走Binder ### 3.2 变更通知机制:条件变量 + 快照 Diff 通知流程: | 步骤 | 执行方 | 操作 | 说明 | |------|--------|------|------| | 1 | Owner | 写入数据 → `changeSeq++` | 单调递增序列号,标记数据版本 | | 2 | Owner | `pthread_cond_broadcast()` | 广播条件变量,唤醒所有等待的 Client | | 3 | Client | `pthread_cond_wait()` 返回 | Polling 线程被内核唤醒 | | 4 | Client | 比较 `changeSeq` | 对比 `seqBefore != seqAfter`,防止虚假唤醒 | | 5 | Client | 快照 Diff | 遍历最新数据 vs 本地快照,识别 new/died/frozen/unfrozen/cookieChanged | | 6 | Client | JNI 回调 | `handleNativeDataChangeNotify()` → `StateChangeListener` | **关键约束与应对**: | 约束 | 设计应对 | |------|---------| | `pthread_cond_wait` 存在虚假唤醒 | 引入 `changeSeq` 单调递增序列号,唤醒后比较 seqBefore != seqAfter,减少无效的数据投递 | | Client 无监听器时不应浪费资源 | 快照按需懒分配:仅在 `addStateChangeListener` 首次调用时 `malloc`,`removeStateChangeListener` 到空时 `free` | | Polling 线程只应启动一次 | `AtomicBoolean.compareAndSet` 保证多线程下只创建一个 polling 线程 | | 通知期间锁不能长期持有 | Diff 计算在锁内完成,JNI 回调在锁外执行,避免死锁 | ### 3.3 DIED 进程清理 进程标记为 `STATE_DIED` 后立即调用 `compactDiedProcessesLocked()` 压缩数组,保证: - 数组始终紧凑,无空洞 - `processCount` 准确反映有效条目数 - 遍历性能不因死亡进程残留而退化 ### 3.4 数据区与锁区分页隔离 ``` ┌──────────────────────────────────┐ Page 0 (4096 bytes, Client mprotect 为只读) │ ProcessStates (3604 bytes) │ changeSeq + processCount + states[300] │ padding (492 bytes) │ 填充到页边界 ├──────────────────────────────────┤ Page 1 (4096 bytes, 可读写) │ pthread_mutex_t lock (40 bytes) │ 跨进程互斥锁 │ pthread_cond_t cond (48 bytes) │ 跨进程条件变量 │ padding (剩余) │ └──────────────────────────────────┘ 总大小: 2 × PAGE_SIZE = 8192 bytes ``` **分页设计逻辑** - `mprotect` 的粒度是页(4096 bytes) - 数据区设为 `PROT_READ` 防止 Client 篡改,但锁区必须保持 `PROT_READ | PROT_WRITE` 以支持 `pthread_mutex_lock` - 分页后,Client 端 `nativeOpen` 时对数据区所在页调用 `mprotect(PROT_READ)`,锁区自然保持读写 ### 3.5 Java 层 Listener 线程 | 设计要点 | 说明 | |----------|------| | **按需创建 Polling 线程** | 只有 Client 需要监听数据时才创建,仅需查询时按需调用接口即可,不创建额外线程 | | **并发控制** | `mListeners` 的添加/移除/遍历全部在 `synchronized(mListeners)` 保护下 | | **单次启动保证** | Polling 线程启动由 `AtomicBoolean.compareAndSet` 保证单次,运行在客户端进程内部 | | **JNI 回调线程安全** | `handleNativeDataChangeNotify` 在主 polling 线程中执行,通过 `synchronized(mListeners)` 保护遍历 | --- ## 4. 性能设计指标 ### 4.1 内存占用分析 | 组件 | 大小 | 说明 | |------|------|------| | `ProcessStates` | **3604 bytes** | 2×int + 300×ProcessState(12 bytes each) | | Padding | 492 bytes | 对齐到 4096 页边界 | | `pthread_mutex_t` | 40 bytes | 跨进程互斥锁(Linux/ARM64) | | `pthread_cond_t` | 48 bytes | 跨进程条件变量(Linux/ARM64) | | **总共享内存** | **8192 bytes (2 pages)** | 2 × PAGE_SIZE | ### 4.2 内存块大小的考虑 - **`MAX_PROCESSES = 300`**:覆盖大部分 Android 设备上同时运行的进程数上限 - 典型设备后台进程数 ≤ 200,300 留有足够余量 - 此时 `sizeof(ProcessStates) = 3604`,padding 仅 492 bytes,**浪费率 12.0%** - **为什么不是 256?** `256 × 12 + 8 = 3080`,padding = 1016,浪费率 24.8% - **为什么不是 512?** `512 × 12 + 8 = 6152`,需要 2 页数据区 + 1 页锁区 = 3 页(12KB),浪费率 47.1%,且进程数需求不大 - **300 是最优选择**:在容量和内存效率之间取得最佳平衡, 可以适当扩容到300+ ### 4.3 延时对比 在独立 Service 进程下的实测数据(10000 次采样): | 访问方式 | 平均单次耗时 | 说明 | |----------|-------------|------| | **本地共享内存直接读取** | **~7 μs** | `mAppFrozenState.getProcessCookie()` 本地调用 | | **IPC (AIDL Binder) 往返** | **~96 μs** | `mService.getProcessCookie()` 跨进程调用 | | **IPC / 本地比值** | **~13.5x** | 共享内存方案有显著性能优势 | ### 4.4 延时分解(本地读取路径) | 步骤 | 操作 | 耗时 | |------|------|------| | 1 | JNI 调用(Java → Native 边界) | ~1.5μs | | 2 | `pthread_mutex_lock` 获取跨进程锁 | ~0.5μs | | 3 | 数组遍历查找 pid | ~0.3μs | | 4 | 读取 ProcessState 数据(内存访问) | ~0.2μs | | 5 | `pthread_mutex_unlock` 释放锁 | ~0.5μs | | 6 | JNI 返回(Native → Java 边界) | ~0.2μs | | | **总计(含 Java↔Native 边界开销)** | **~7μs** | > 本地读取主要为锁操作和内存读写,无 IPC 开销,因此远优于 Binder 方案。 ### 4.5 通知延时 | 阶段 | 操作 | 说明 | |------|------|------| | 1 | Owner 写入数据 | 更新共享内存中的进程状态 | | 2 | `pthread_cond_broadcast` | Owner 广播条件变量,唤醒所有等待的 Client | | 3 | Client `pthread_cond_wait` 返回 | 内核唤醒阻塞线程 | | 4 | Diff 计算 | Client 对比最新数据与本地快照 | | 5 | JNI 回调 | 通过 `handleNativeDataChangeNotify` 通知 Java 层 | - **总延时**:无竞争场景下 < **200μs**(主要开销在 `pthread_cond_wait` 内核唤醒 + JNI 回调) - **设计取舍**:通知延时虽比本地查询(~7μs)大,但优势在于**推送模式**——Client 无需轮询即可感知变化。同时保留了按需查询路径(~7μs),供对实时性要求更高的场景使用 --- ## 5. 安全设计 ### 5.1 威胁模型 | 威胁 | 描述 | 风险等级 | |------|------|---------| | 恶意 Client 篡改数据 | Client 进程被攻破后直接修改共享内存中的进程状态 | 高 | | 恶意进程伪造 fd | 第三方应用通过猜测 Ashmem 名称打开同一块内存 | 高 | | 数据竞争导致状态不一致 | 无锁并发访问导致读到中间态数据 | 中 | | 进程异常退出残留 | Owner 进程崩溃后共享内存未释放 | 低 | ### 5.2 防护措施 | 防护 | 机制 | 效果 | |------|------|------| | **Client 只读保护** | `mprotect(addr, dataSize, PROT_READ)` | Client 端任何对数据区的写入触发 SIGSEGV,进程崩溃 | | **Owner 独占写入** | 架构约束:只有 Service 进程是 Owner,持有完整读写权限 | 单一可信写入源 | | **Ashmem 命名随机化** | `UUID.randomUUID().toString()` 作为 Ashmem 名称 | 第三方无法猜测内存名称进行未授权访问 | | **跨进程互斥锁** | `pthread_mutex_t` + `PTHREAD_PROCESS_SHARED` | 读写原子性,杜绝数据竞争 | | **fd 通过 Binder 传递** | `ParcelFileDescriptor` 在 AIDL 中传递 | 利用 Binder 的权限检查,只有绑定到 Service 的进程才能获取 fd | | **内核级生命周期管理** | Ashmem fd 的引用计数由内核维护 | Owner 和所有 Client 的 fd 关闭后,内存自动回收,无残留风险 | | **最终回收保障** | `finalize()` 兜底调用 `close()` | 即使忘记显式关闭,GC 时也会释放资源 | ### 5.3 安全层级总览 | 层级 | 机制 | 防护目标 | |------|------|----------| | **Layer 1** | Binder 权限控制 | 只有 `bindService` 的进程才能获取 Ashmem fd | | **Layer 2** | Ashmem 命名随机化 | UUID 防止其他进程通过名称打开同一块内存 | | **Layer 3** | `mprotect` 硬件级写保护 | Client 端数据区 `PROT_READ`,写入触发 SIGSEGV | | **Layer 4** | 跨进程互斥锁 | `pthread_mutex` 保证读写原子性 | | **Layer 5** | 内核资源管理 | Ashmem 引用计数,进程退出自动回收 | --- ## 6. 架构与内存布局 ### 6.1 系统架构图 ![系统架构图](images/architecture.png) ### 6.2 内存布局图 ![内存布局图](images/memory_layout.png) ### 6.3 数据流图 ![数据流图](images/data_flow.png) --- ## 7. 项目结构 ``` app/ ├── src/main/ │ ├── aidl/com/example/appstatemanager/ │ │ ├── ISharedMemoryService.aidl # Binder IPC 接口定义 │ │ └── AppFrozenState.aidl # Parcelable 声明 │ ├── java/com/example/appstatemanager/ │ │ ├── MainActivity.java # 主界面 + 测试入口 │ │ ├── SharedMemoryService.java # Owner 进程 Service │ │ ├── SharedMemoryClient.java # Client 端业务逻辑 + 测试用例 │ │ └── AppFrozenState.java # 共享内存 Java 封装 + 监听器 │ ├── cpp/ │ │ ├── shared_memory.cpp # JNI Native 实现(核心逻辑) │ │ ├── shared_memory.h # C++ 头文件 │ │ └── CMakeLists.txt # CMake 构建配置 │ ├── res/layout/ │ │ └── activity_main.xml # 主界面布局 │ └── AndroidManifest.xml └── build.gradle.kts ``` ### AIDL 接口 ```java interface ISharedMemoryService { AppFrozenState getAppFrozenState(); // 获取共享内存对象(传递 fd) boolean freezeProcess(int pid, boolean freeze); // 冻结/解冻进程 boolean queryProcessFrozenState(int pid); // 查询冻结状态 boolean setProcessCookie(int pid, int cookie); // 设置 Cookie int getProcessCookie(int pid); // 读取 Cookie int getCount(); // 获取条目数 void dumpAll(); // 打印全部状态 boolean createProcess(int pid); // 模拟进程创建 boolean killProcess(int pid); // 模拟进程死亡 } ``` ### 进程状态定义 ```cpp enum AppState { STATE_NONE = 0, // 未使用 STATE_NEW = 1, // 新创建 STATE_FROZEN = 2, // 已冻结 STATE_THAWED = 3, // 已解冻 STATE_DIED = 4, // 已死亡 }; struct ProcessState { pid_t pid; // 进程 ID (4 bytes) AppState state; // 进程状态 (4 bytes) int cookie; // 附加数据 (4 bytes) }; // 共 12 bytes ``` ### 并发安全机制汇总 | 同步机制 | 作用 | 层级 | |----------|------|------| | `pthread_mutex_t` (PTHREAD_PROCESS_SHARED) | 跨进程互斥锁,保护数据读写 | C++ Native | | `pthread_cond_t` (PTHREAD_PROCESS_SHARED) | 跨进程条件变量,Owner→Client 通知 | C++ Native | | `mprotect(PROT_READ)` | Client 端数据区只读保护 | C++ Native | | `AtomicBoolean` | Java 层 Polling 线程只启动一次 | Java | | `synchronized(mListeners)` | Java 层 Listener 列表线程安全 | Java | --- ## 8. 测试用例 通过 UI 按钮触发,结果输出到日志区: | 测试 | 按钮 | 验证内容 | |------|------|----------| | **数据持久化** | `test Data Persistence` | Server 写入 → Client 本地读取 → 两端 dump 对比,逐 pid 校验 cookie 一致性 | | **并发访问** | `test Concurrent Access Data` | 6 线程 × 20 轮高并发随机写入,最终跨端逐 pid 校验 cookie 值一致性 | | **进程观察者** | `test Process Observer` | 30 轮随机进程生命周期操作,验证 Listener 正确收到 Created/Died/Frozen/Unfrozen/CookieChanged 回调 | | **访问延时对比** | `test Access Latency` | 10000 次本地共享内存读取 vs Binder IPC 读取,对比延时 | --- ## 9. 构建与运行 ### 前置条件 - Android Studio Hedgehog (2023.1.1) 或更高版本 - NDK 26+(CMake 原生构建) - Android 8.0+ (API 26+) 设备/模拟器 ### 构建步骤 ```bash git clone https://gitee.com/applematrix/app-state-manager.git cd app-state-manager ./gradlew assembleDebug ``` ### 运行测试 1. 安装 APK 到设备 2. 打开应用,点击 **bind service** 建立连接 3. 依次点击测试按钮观察日志输出 --- ## 技术栈 | 技术 | 用途 | |------|------| | **AIDL + Binder** | 跨进程服务绑定、Ashmem fd 传递、写操作 IPC | | **Ashmem** | Android 匿名共享内存,内核管理生命周期 | | **mmap** | 将 Ashmem 映射到进程地址空间 | | **mprotect** | Client 端数据区只读保护 | | **pthread_mutex / pthread_cond** | 跨进程同步(PTHREAD_PROCESS_SHARED) | | **JNI (C++17)** | Java 与 Native 层桥接 | | **CMake + NDK** | 原生代码构建 | | **ConstraintLayout + ViewBinding** | UI 布局 | | **Java 11** | Android 应用层 | --- ## License MIT