4. SharedPreferences 现有代码审计:识别所有 SP 引用、分析数据类型、记录默认值、梳理读写频率
在开始任何迁移之前,必须对项目中现有的 SharedPreferences 代码进行全面审计。本章将指导你系统性地完成审计工作,输出一份完整的 SP 清单,为后续的 DataStore 迁移提供精确的输入数据。
4.1 审计目标与产出物
审计的核心目标是回答三个问题:
- 谁在用 SP? —— 所有
SharedPreferences实例的引用位置、名称、作用域。 - 存了什么? —— 每个 key 对应的数据类型、默认值、读写频率。
- 怎么用的? —— 是单线程读写还是多线程并发?是否跨进程?有无 commit/apply 混用?
审计产出物清单:
- SP 引用总表(文件名 → 实例名 → key → 类型 → 默认值)
- 读写频率热力图(高频 key 的读写次数统计)
- 异常模式检测(跨进程、同步阻塞、错误默认值)
- 迁移优先级排序(按重构风险和收益)
4.2 审计步骤
4.2.1 搜索所有 SP 引用点
使用 IDE 全局搜索或脚本找出所有 getSharedPreferences、PreferenceManager.getDefaultSharedPreferences、SharedPreferences.Editor 的调用。将结果按模块整理。
| SP 文件名 | 实例变量名 | 获取方式 | 作用域 |
|---|---|---|---|
| user_prefs.xml | userPreferences | context.getSharedPreferences("user_prefs", MODE_PRIVATE) | 全局单例 |
| default | defaultPrefs | PreferenceManager.getDefaultSharedPreferences() | Application 级 |
| feature_a_config | featurePrefs | context.getSharedPreferences("feature_a_config", ...) | Activity 内局部 |
4.2.2 提取每个 key 的数据字典
对每个 SP 文件,逐个 key 记录:名称、数据类型、默认值、读写发生的源文件与行号。
// 示例审计记录(user_prefs.xml)
key: "is_logged_in" type: boolean default: false read: LoginActivity.java:42,ProfileFrag.java:88 write: AuthManager.java:155
key: "user_id" type: String default: "" read: HomeVM.java:23,ProfileVM.java:67 write: AuthManager.java:160
key: "last_sync_timestamp" type: long default: 0L read: SyncWorker.java:34 write: SyncWorker.java:55
key: "theme_mode" type: int default: 0 read: SettingsActivity.java:102 write: SettingsActivity.java:210
4.2.3 统计读写频率
手动或通过代码插桩统计每个 key 在典型用户会话中的读写次数。高频 key 是迁移的主要关注点。
| key | 单次启动读取次数 | 单次启动写入次数 | 读写比 |
|---|---|---|---|
| is_logged_in | 3 | 1 | 3:1 |
| user_id | 2 | 1 | 2:1 |
| last_sync_timestamp | 1 | 1 | 1:1 |
| theme_mode | 2 | 1 | 2:1 |
4.3 数据类型与默认值分析
DataStore 支持的数据类型有限(基本类型 + 自定义协议缓冲区),而 SP 允许任意 Java 基本类型和 String/Set<String>。审计时需标记无法直接映射的类型(如 float、long 的特定精度要求、复杂对象序列化为 String 的情况)。
⚠️ 注意! 如果 SP 中存储了 JSON String,迁移时需要考虑是否保留为 String 还是转入 DataStore 的自定义类型(Proto DataStore)。后者需要额外定义 protobuf schema,增加迁移复杂度。
默认值陷阱
- SP 的
getX(key, default)每次调用都需要传递默认值,不同调用点可能传递不同默认值 → 不一致风险。 - DataStore 的默认值在读取时通过流的初始值或类型默认值决定,迁移时必须将所有 SP key 的默认值统一记录。
- 如果某个 key 的默认值被不同地方写死为不同值(如
getString("key", "")与getString("key", null)),需要在审计阶段暴露并统一。
// 审计发现:同一个 key 在不同位置使用了不同默认值
// LoginActivity.java:45
boolean loggedIn = prefs.getBoolean("is_logged_in", false);
// SplashActivity.java:23
boolean loggedIn = prefs.getBoolean("is_logged_in", true); // 默认值不同!
4.4 读写频率与并发风险
通过静态代码分析 + 运行时 profiling 评估:
- 是否有
apply()vscommit()混用导致 UI 线程阻塞? - 是否有多个线程同时写入同一个 SP 文件?
- 是否有跨进程读取(
MODE_MULTI_PROCESS)?
高频写入的 key(如日志记录、位置更新)需要优先考虑使用 DataStore 的异步 API,避免 SP 的同步写造成 ANR。
4.5 审计结果汇总——SP 审计清单(示例)
| SP 文件名 | key 总数 | 高频 key(读写 >10次/会话) | 有默认值不一致 | 跨进程 | 迁移优先级 |
|---|---|---|---|---|---|
| user_prefs.xml | 12 | is_logged_in, user_id, | 是(is_logged_in) | 否 | P0 |
| default | 8 | analytics_opt_in | 否 | 否 | P1 |
| feature_a_config | 5 | none | 否 | MODE_MULTI_PROCESS | P2(需评估跨进程必要性) |
4.6 审计流程图
4.7 常见错误与解决
💡 编码技巧: 使用 Android Studio 的“Find in Path”(Ctrl+Shift+F)搜索
getSharedPreferences|getBoolean|getString|getInt|getLong|getFloat|getStringSet 即可覆盖几乎所有 SP 读取点。写入点搜索 editor\.put|\.apply\(\)|\.commit\(\)。
- 漏掉第三方库的 SP:部分 SDK 会通过
PreferenceManager创建自己的 SP,审计时需检查所有依赖库的初始化代码。 - 混淆默认值:同一个 key 在不同 Activity 中传入了不同的默认值,导致迁移后行为不一致。审计阶段应该记录最权威的默认值(通常来自业务定义类或常量类)。
- 忽略 apply/commit 混用:
commit()会阻塞 UI 线程,高频写入路径应统一改为apply(),迁移至 DataStore 后则自动异步处理。
4.8 审计报告模板(推荐)
使用以下结构组织你的审计输出:
# SharedPreferences Audit Report
## 项目名称: ExampleApp
## 审计日期: 2025-03-10
## 总SP文件数: 3
## 总key数: 25
### SP文件: user_prefs.xml
| key | 类型 | 默认值 | 读取次数 | 写入次数 | 读取位置 | 写入位置 | 风险标记 |
|-----|------|--------|----------|----------|----------|----------|----------|
| is_logged_in | boolean | false | 3 | 1 | Login, Profile | Auth | 默认值不一致 |
| user_id | String | "" | 2 | 1 | Home, Profile | Auth | 无 |
| ... | ... | ... | ... | ... | ... | ... | ... |
### 迁移优先级排序
1. user_prefs.xml (P0) — 高频读写 + 默认值问题
2. default (P1) — 低频稳定
3. feature_a_config (P2) — 跨进程需评估
🛑 注意: 审计不是一次性工作。在后续代码变更中,每次新增 SP key 都需要同步更新审计清单。建议将审计报告纳入 CI 检查的一部分(例如通过 Lint 规则或自定义脚本)。