4. SharedPreferences 现有代码审计:识别所有 SP 引用、分析数据类型、记录默认值、梳理读写频率

在开始任何迁移之前,必须对项目中现有的 SharedPreferences 代码进行全面审计。本章将指导你系统性地完成审计工作,输出一份完整的 SP 清单,为后续的 DataStore 迁移提供精确的输入数据。

4.1 审计目标与产出物

审计的核心目标是回答三个问题:

  • 谁在用 SP? —— 所有 SharedPreferences 实例的引用位置、名称、作用域。
  • 存了什么? —— 每个 key 对应的数据类型、默认值、读写频率。
  • 怎么用的? —— 是单线程读写还是多线程并发?是否跨进程?有无 commit/apply 混用?
审计产出物清单:
  1. SP 引用总表(文件名 → 实例名 → key → 类型 → 默认值)
  2. 读写频率热力图(高频 key 的读写次数统计)
  3. 异常模式检测(跨进程、同步阻塞、错误默认值)
  4. 迁移优先级排序(按重构风险和收益)

4.2 审计步骤

4.2.1 搜索所有 SP 引用点

使用 IDE 全局搜索或脚本找出所有 getSharedPreferences、PreferenceManager.getDefaultSharedPreferences、SharedPreferences.Editor 的调用。将结果按模块整理。

SP 文件名实例变量名获取方式作用域
user_prefs.xmluserPreferencescontext.getSharedPreferences("user_prefs", MODE_PRIVATE)全局单例
defaultdefaultPrefsPreferenceManager.getDefaultSharedPreferences()Application 级
feature_a_configfeaturePrefscontext.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_in313:1
user_id212:1
last_sync_timestamp111:1
theme_mode212: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() vs commit() 混用导致 UI 线程阻塞?
  • 是否有多个线程同时写入同一个 SP 文件?
  • 是否有跨进程读取(MODE_MULTI_PROCESS)?

高频写入的 key(如日志记录、位置更新)需要优先考虑使用 DataStore 的异步 API,避免 SP 的同步写造成 ANR。

4.5 审计结果汇总——SP 审计清单(示例)

SP 文件名key 总数高频 key(读写 >10次/会话)有默认值不一致跨进程迁移优先级
user_prefs.xml12is_logged_in, user_id, 是(is_logged_in)否P0
default8analytics_opt_in否否P1
feature_a_config5none否MODE_MULTI_PROCESSP2(需评估跨进程必要性)

4.6 审计流程图

SharedPreferences 代码审计流程 ① 搜索所有 getSharedPreferences ② 提取每个key的数据字典 ③ 分析数据类型与默认值 ④ 统计读写频率 ⑤ 标记并发与跨进程风险 ⑥ 输出审计清单与迁移优先级 每个步骤的输出都将作为下一章迁移实现的输入——审计不完整则迁移不可靠

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 规则或自定义脚本)。