Administrator
发布于 2026-08-25 / 18 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 31 篇:Android 存储演进:从 SharedPreferences 到 DataStore 的迁移之路

上一篇我们聊了 KMM 的跨平台实践,这篇把视角拉回本地存储。SharedPreferences 陪伴 Android 开发者十几年了,但 Google 自己也在推 DataStore。到底要不要迁移?怎么迁?踩了什么坑?

为什么 SharedPreferences 不够用了

先说结论:SP 不是不能用,但在异步写入、类型安全和事务一致性上有先天缺陷。

SP 的几个老问题:

  1. 同步读取阻塞主线程。getSharedPreferences().getString() 在首次加载时会触发磁盘 IO,如果文件大,ANR 不是吓你的。

  2. apply() 异步写盘但不可控。没有回调告诉你写完了,也没有错误处理。commit() 同步写盘但阻塞。

  3. 不支持类型安全。所有 get/put 都是裸 key-value,key 拼错只有运行时才炸。

  4. 事务粒度太粗。多次 put 只在 commit/apply 时一次性写入,但中途如果崩溃,数据状态不确定。

DataStore 两种模式

Google 提供了两个实现:

维度

Preferences DataStore

Proto DataStore

类型安全

否(类似 SP 的 key-value)

是(基于 Protocol Buffers)

Schema 演进

不需要预定义 schema

需要写 .proto 文件

迁移成本

低,API 接近 SP

中,需要学 proto 语法

适用场景

简单配置项

复杂结构化数据

大多数项目从 SP 迁移时,先用 Preferences DataStore 就够了。

基础用法

// 创建 DataStore
private val Context.dataStore by preferencesDataStore("settings")

// 定义 key
val KEY_DARK_MODE = booleanPreferencesKey("dark_mode")
val KEY_USER_ID = stringPreferencesKey("user_id")

// 写入
suspend fun saveSettings(darkMode: Boolean, userId: String) {
    dataStore.edit { preferences ->
        preferences[KEY_DARK_MODE] = darkMode
        preferences[KEY_USER_ID] = userId
    }
}

// 读取(Flow 响应式)
val darkMode: Flow<Boolean> = dataStore.data
    .map { it[KEY_DARK_MODE] ?: false }

注意几个点:edit 是 suspend 函数,天然支持协程。读数据返回的是 Flow,可以跟 Compose 或 LiveData 无缝衔接。如果你只需要一次性读取,用 dataStore.data.first()。

从 SP 迁移

DataStore 内置了 SP 迁移工具:

private val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, "old_prefs_name") { sp, _ ->
            // 把 SP 的数据映射到 DataStore
            val darkMode = sp.getBoolean("dark_mode", false)
            mapOf(KEY_DARK_MODE to darkMode)
        })
    }
)

迁移发生在 DataStore 首次初始化时,执行完后 SP 文件不会被删除(你得自己删)。迁移是原子的——要么全部成功,要么回滚,不会出现半迁移状态。

Proto DataStore 的优势

当你的存储对象有嵌套结构时,Preferences DataStore 要手动管理多个 key,容易出错。Proto DataStore 直接序列化一个对象:

// user_prefs.proto
syntax = "proto3";

message UserPreferences {
  bool dark_mode = 1;
  string user_id = 2;
  int32 font_size = 3;
}
class UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue = UserPreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        return UserPreferences.parseFrom(input)
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        t.writeTo(output)
    }
}

好处是类型安全、字段变更可追溯、序列化性能比 JSON 快不少。代价是要引入 protobuf 编译器。

实际迁移建议

如果你还在犹豫要不要迁,我的建议:

  • 新项目直接上 DataStore,没有历史包袱就别用 SP 了。

  • 存量项目按模块逐步迁移,不要一次性全换。先迁配置类数据,再迁用户偏好。

  • 如果数据结构复杂且稳定,直接用 Proto DataStore,省得后面再迁一次。

  • SP 和 DataStore 可以共存,迁移期间两者并行,新代码用 DataStore,旧代码逐步替换。

一个踩坑记录

apply() 在 SP 里是异步的,但 DataStore 的 edit 是 suspend。如果你在 onPause() 里调 edit 而没有用 runBlocking,数据可能写不进去。解决方案是用 lifecycleScope.launch 确保协程有足够时间执行,或者用 DataStore 的 updateData 配合 runBlocking 做兜底。

小结

SharedPreferences 不是不能用,但 DataStore 在类型安全、响应式读取、错误处理上确实更好。迁移成本不高,建议新项目直接上,存量项目按模块逐步替换。

下一篇我们聊聊 Android 消息机制的核心:Handler/Looper/MessageQueue 到底怎么运作的。


评论