第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 选型决策流程图

存储引擎选型决策图 离线缓存存储引擎选型决策 开始选型 数据量 < 1MB? 是 需要复杂查询? Room 是否兼容旧 SP? 是 (SP 迁移) DataStore SharedPreferences 数据量 > 1MB 结构化? Room 文件存储 决策铁律:优先 DataStore(新项目)、Room(复杂缓存)、文件存储(大文件)。

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 部署策略建议

  1. 新建项目或模块:优先选用 DataStore 作为键值存储,Room 作为关系型缓存。
  2. 旧项目迁移:从 SP 迁移至 DataStore 时,使用 SharedPreferencesMigration 无痛过渡。
  3. 大文件/二进制数据:统一使用内部 getDir() 目录并增加 LRU 淘汰策略。
  4. 多进程场景:必须使用 Room 或自定义 ContentProvider,杜绝 SP。
终极选型口诀:小量简单用 DataStore,复杂关系用 Room,文件大块直接 IO,老项目遗留慎用 SP。