第2章 SharedPreferences优化:底层原理与替代方案
SharedPreferences,简称SP,几乎是每个Android开发者最早接触的存储方案。但说实话,它也是坑最多的一个。我见过太多项目因为SP用得不当,导致启动卡顿、数据丢失,甚至ANR。今天我们就把它彻底讲透。
2.1 SP的底层原理:XML解析与缓存
SP的本质是什么?说白了,就是一个XML文件。你每次调用putString()、getInt(),底层都在操作这个XML。
我简单梳理一下它的工作流程:
- 应用启动时,SP会在主线程读取整个XML文件
- 用XmlPullParser解析,把键值对加载到内存的Map中
- 后续的get操作,直接从内存Map读取,不再读文件
- put操作会先修改内存Map,然后异步写回文件
嗯,这里有个关键点——全量加载。你想想看,哪怕你只存了一个"isLogin"字段,SP也会把整个XML文件从头到尾解析一遍。如果这个文件有几百KB,甚至几MB,那启动耗时就很可观了。
核心结论:SP的get操作是O(1)的,但首次加载是O(n)的。n就是XML文件的大小。
我在项目中遇到过这样一个案例:某个模块的SP文件存了用户的操作日志,大概有2000多条key-value。结果每次冷启动,这个SP的加载耗时都在300ms以上。用户反馈说"点开应用要等半天",其实就是这个原因。
2.2 跨进程安全与ANR问题
先说说跨进程。SP官方是不推荐跨进程使用的。虽然它提供了MODE_MULTI_PROCESS这个flag,但我建议你直接忽略它——这个flag在API 23之后基本就没用了。它只是在每次get操作时重新读一下文件,但写入还是异步的,数据一致性完全无法保证。
警告:千万不要用SP做跨进程通信。我曾经接手过一个项目,两个进程同时读写同一个SP文件,结果数据丢失、文件损坏的情况频繁出现。最后花了整整一周才把所有问题排查清楚。
再说ANR。SP的ANR问题主要出在apply()和commit()上。
| 方法 | 特点 | ANR风险 |
|---|---|---|
| commit() | 同步写入,直接写磁盘 | 高(主线程调用时) |
| apply() | 异步写入,但会阻塞QueuedWork | 中(Activity/Fragment onStop时) |
为什么apply()也会导致ANR?因为Android在Activity的onStop()中,会等待所有apply()任务执行完毕。如果你在主线程连续调用了多次apply(),或者写入的数据量很大,那这个等待时间就可能超过5秒,触发ANR。
我曾经在线上监控到一个案例:某个页面退出时,SP的apply()等待时间达到了3.8秒。排查后发现,是因为一个循环里调用了100多次putString(),每次都是独立apply()。嗯,这里要记住一个原则——批量操作,统一提交。
避坑指南:如果你需要在短时间内写入大量数据,建议先调用SharedPreferences.Editor的put方法,最后只调用一次apply()或commit()。
2.3 推荐替代方案:DataStore
既然SP有这么多问题,那有没有更好的选择?Google官方推荐的是DataStore。它分为两种:
- Preferences DataStore:类似SP,存储键值对
- Proto DataStore:基于Protocol Buffers,存储类型化对象
DataStore相比SP,有几个明显的优势:
- 基于协程:所有读写操作都是异步的,不会阻塞主线程
- 事务性:要么全部成功,要么全部失败,不会出现数据不一致
- 类型安全:Proto DataStore可以定义数据模型,编译时检查
- 无ANR风险:因为所有操作都在协程中执行
来看一个简单的例子:
// 1. 添加依赖
implementation "androidx.datastore:datastore-preferences:1.0.0"
// 2. 创建DataStore实例
val Context.dataStore by preferencesDataStore(name = "user_settings")
// 3. 写入数据
suspend fun saveUserName(name: String) {
dataStore.edit { preferences ->
preferences[stringPreferencesKey("user_name")] = name
}
}
// 4. 读取数据
val userName: Flow<String?> = dataStore.data.map { preferences ->
preferences[stringPreferencesKey("user_name")]
}
你看,DataStore返回的是Flow,可以很方便地用collect观察数据变化。而且它内部做了很多优化,比如数据去重、缓存管理等。
不过DataStore也不是银弹。我个人的经验是:
- 如果项目已经全面使用Kotlin协程,那直接上DataStore
- 如果项目还是Java为主,或者对协程不熟悉,那可以继续用SP,但要注意我前面说的那些坑
- 对于简单的配置项(比如开关、设置项),SP其实够用,关键是要控制文件大小
我的建议:新项目直接用DataStore。老项目可以逐步迁移,先从那些频繁读写、容易引发ANR的SP文件开始改。
最后总结一下。SP的底层原理其实不复杂,但它的设计缺陷导致了ANR和跨进程安全问题。DataStore作为官方替代方案,在异步性和安全性上做了很大改进。不过无论用哪种方案,都要记住一点——存储不是免费的,每一次读写都有代价。合理规划你的数据存储策略,比纠结用哪个库更重要。