第3章 存储引擎选型与对比
离线缓存方案的核心在于选择合适的本地持久化引擎。Android 平台提供了 SharedPreferences、Room、DataStore 以及原生 文件存储 四种主流方案。本章将从原理、性能、线程安全、易用性、适用场景等维度进行横向对比,并给出基于项目规模和数据复杂度的选型决策流程。
3.1 引擎概览与核心差异
| 特性 | SharedPreferences | Room | DataStore | 文件存储 |
|---|---|---|---|---|
| 数据格式 | 键值对(XML) | 关系型表(SQLite) | 键值对(Proto / Preferences) | 任意格式(JSON、二进制等) |
| 线程安全 | 非线程安全(需同步) | 内置协程/Flow 支持 | Flow + 协程,完全异步 | 需开发者自行处理 |
| 类型安全 | 弱类型(getString等) | 强类型(编译期校验) | 强类型(Proto 定义) | 取决于序列化方式 |
| 查询能力 | 仅通过 key 取值 | SQL / Flow / 分页 | 流式观察,无复杂查询 | 需自行解析 |
| 存储量级 | MB 级别(不推荐大容量) | GB 级(SQLite 优势) | KB ~ 数 MB(小数据) | 无上限(依赖磁盘) |
| 异步支持 | 无原生异步 | LiveData / Flow / RxJava | 基于 Flow 的响应式 | 需自定义线程 |
3.2 原理与适用场景
3.2.1 SharedPreferences(SP)
基于 XML 文件缓存在应用内部目录,每次 commit() 或 apply() 会同步/异步写入整个文件。适用于少量简单配置,如用户偏好设置、开关标志。但在多进程或高频写操作时容易导致 ANR 和数据丢失。
已知问题:SP 在跨进程场景下未提供任何一致性保证;apply() 虽然是异步,但在 Activity 生命周期回调中频繁调用仍可能阻塞主线程。
// 传统 SharedPreferences 用法
val sp = context.getSharedPreferences("app_config", Context.MODE_PRIVATE)
sp.edit().putString("username", "alice").apply()
// 读取
val name = sp.getString("username", "default")
3.2.2 Room(SQLite ORM)
Google 官方推出的数据库抽象层,编译期生成 DAO 实现。适用于结构化数据、复杂查询、离线缓存管理。支持 Flow 与协程,可配合 Paging 3 实现列表缓存。
// Entity 定义
@Entity(tableName = "articles")
data class Article(
@PrimaryKey val id: Int,
val title: String,
val content: String
)
// DAO 接口
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles WHERE id = :id")
fun getArticle(id: Int): Flow<Article>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(articles: List<Article>)
}
// Database 类
@Database(entities = [Article::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
}
3.2.3 DataStore(Jetpack)
Google 推出的 SP 替代方案,分为 Preferences DataStore(键值对)和 Proto DataStore(强类型)。使用 Flow 实现异步读取,内部通过事务和协程保持一致性,彻底消灭 ANR。
// Preferences DataStore
val Context.dataStore by preferencesDataStore(name = "user_settings")
val USER_NAME = stringPreferencesKey("user_name")
suspend fun saveName(context: Context, name: String) {
context.dataStore.edit { preferences ->
preferences[USER_NAME] = name
}
}
val nameFlow: Flow<String> = context.dataStore.data.map { preferences ->
preferences[USER_NAME] ?: ""
}
迁移建议:如果项目已使用 SP 且需要渐进式迁移,可利用 DataStore 的
SharedPreferencesMigration 自动迁移数据。
3.2.4 文件存储(File I/O)
直接读写应用内部或外部存储目录中的文件。适用于大文件、图片、视频、JSON/Protobuf 序列化全文。可配合 Kotlin 的序列化库或 Gson 实现结构存取。
// 写入 JSON 到内部存储
val jsonString = Json.encodeToString(myObject)
context.openFileOutput("cache.json", Context.MODE_PRIVATE).use {
it.write(jsonString.toByteArray())
}
// 读取
val content = context.openFileInput("cache.json").bufferedReader().use { it.readText() }
3.3 选型决策流程图
3.4 性能对比与调优
| 指标 | SharedPreferences | Room | DataStore | 文件存储 |
|---|---|---|---|---|
| 写入延遲(单次 1KB) | 2~5 ms(apply) | 1~3 ms(事务内) | 0.5~1 ms(协程) | 0.5~2 ms(缓存流) |
| 内存占用(100条数据) | 全量加载,约 150KB | 分页加载,约 50KB | 流式加载,约 30KB | 取决于解析器 |
| 主线程安全 | 危险(apply 仍可能卡顿) | 安全(协程保证) | 安全 | 需异步 |
调优要点:
- 使用 DataStore 时,避免在热路径中频繁调用
edit{},可合并写入。 - Room 务必启用
WAL 模式提升并发性能:setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)。 - 文件存储推荐使用
BufferedOutputStream配合DiskLruCache减小随机 IO。
3.5 常见错误与规避
- SP 跨进程竞态:不要在多进程场景使用 SP,改用 ContentProvider 或 Room。
- Room 数据库版本迁移失败:每次修改 Entity 务必编写 Migration 逻辑,否则
IllegalStateException崩溃。 - DataStore 文件损坏:Proto DataStore 如果未处理异常,会导致整个文件不可读;建议配置
catch { … }提供默认值。 - 文件句柄泄漏:文件 I/O 使用
.use { }自动关闭资源。
// Room 迁移示例
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE articles ADD COLUMN author TEXT DEFAULT 'unknown'")
}
}
3.6 部署策略建议
- 新建项目或模块:优先选用 DataStore 作为键值存储,Room 作为关系型缓存。
- 旧项目迁移:从 SP 迁移至 DataStore 时,使用
SharedPreferencesMigration无痛过渡。 - 大文件/二进制数据:统一使用内部
getDir()目录并增加 LRU 淘汰策略。 - 多进程场景:必须使用 Room 或自定义 ContentProvider,杜绝 SP。
终极选型口诀:小量简单用 DataStore,复杂关系用 Room,文件大块直接 IO,老项目遗留慎用 SP。