第4章 本地存储引擎选型:SQLite / Realm / Room 对比、存储策略、性能考量

4.1 离线读写场景下的存储选型要素

在为 API 封装层设计离线读写能力时,本地存储引擎是承载缓存数据、队列请求、离线日志的核心基础设施。选型不当会直接导致读写延迟高、数据不一致、包体积膨胀或跨平台适配困难。选型需从以下维度综合评估:

  • 离线读写能力——是否支持事务、冲突合并、增量同步
  • 性能基准——单次读/写延迟、批量写入吞吐、并发控制
  • 开发效率——API 设计是否自然、是否需手写 SQL、编译期校验
  • 跨平台与生态——对 iOS / Android / Desktop 的支持、社区活跃度
  • 包体积增量——对移动端安装包大小的影响
实践建议:在项目早期搭建离线读写原型,用真实业务数据对三个引擎做压测(单条插入、批量写入、混合读写),避免仅凭文档指标做决策。

4.2 三大存储引擎核心特性对比

维度 SQLite Realm Room
类型 嵌入式关系型(C 库) 对象存储(非关系型) SQLite 抽象层(ORM)
查询方式 原生 SQL 链式 API / 谓词 DAO + @Query 注解
单条读性能 ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️
批量写性能 ⭐️⭐️⭐️(事务合并可优化) ⭐️⭐️⭐️⭐️⭐️(延迟写入) ⭐️⭐️⭐️(依赖 SQLite 事务)
离线事务支持 ACID 完整 ACID + 自动回滚 ACID(靠 SQLite)
跨平台支持 全平台(C 绑定) iOS / Android / JS 主要 Android
包体积增量 ~500 KB ~6 MB ~200 KB(+ SQLite)
学习曲线 中(需 SQL 基础) 低(对象化 API) 中(注解 + SQL 混合)
推荐场景 标准化离线缓存、日志 高并发离线读写、实时同步 Android 原生 MVVM 架构
注意:Realm 在 2023 年宣布逐步停止社区版维护,转向商业化授权。若团队预算有限,建议优先评估 SQLite 或 Room 的长期可持续性。

4.3 存储策略差异分析

不同引擎在离线场景下的存储策略各有侧重,直接影响数据结构设计、同步机制和冲突处理方式。

4.3.1 SQLite——原生 SQL + 事务合并

适合需要标准化 SQL 访问、跨平台复用逻辑的团队。离线写入时依赖显式事务控制:

// SQLite 离线批量写入(C API)
sqlite3 *db;
sqlite3_open("offline.db", &db);
sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, 0);
for (int i = 0; i < batchSize; i++) {
    sqlite3_stmt *stmt;
    sqlite3_prepare_v2(db,
        "INSERT INTO cache(key, value, ts) VALUES(?,?,?)", -1, &stmt, 0);
    sqlite3_bind_text(stmt, 1, key[i], -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 2, val[i], -1, SQLITE_TRANSIENT);
    sqlite3_bind_int64(stmt, 3, now());
    sqlite3_step(stmt);
    sqlite3_finalize(stmt);
}
sqlite3_exec(db, "COMMIT", 0, 0, 0);
sqlite3_close(db);
  • 写策略:单条写入默认自动事务,批量需手动包裹 BEGIN/COMMIT 提升 10 – 20 倍吞吐。
  • 冲突处理:通过 INSERT OR REPLACE 或 ON CONFLICT 子句实现。
  • 离线队列:利用自增 ID 和状态字段实现请求排队与重试。

4.3.2 Realm——对象图 + 延迟写入

Realm 采用零拷贝内存映射文件,写入先缓存到内存再异步刷盘,适合写入频繁的离线场景:

// Realm 离线写入(Kotlin)
val realm = Realm.getDefaultInstance()
realm.beginTransaction()
for (item in offlineQueue) {
    val obj = realm.createObject(OfflineRequest::class.java, item.id)
    obj.method = item.method
    obj.url = item.url
    obj.body = item.body
    obj.retryCount = 0
}
realm.commitTransaction()
realm.close()
  • 写策略:延迟写入,批处理提交,减少 I/O 次数。
  • 冲突处理:基于主键自动合并,支持自定义冲突解决器。
  • 离线队列:内置 RealmResults 实时监听,数据变更自动通知同步层。

4.3.3 Room——SQLite ORM + 协程/Flow 集成

Room 在 SQLite 之上提供编译时 SQL 校验和响应式查询,适合 Android 团队:

// Room 离线缓存 DAO(Kotlin)
@Dao
interface CacheDao {
    @Query("SELECT * FROM cache WHERE key = :key")
    suspend fun get(key: String): CacheItem?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(item: CacheItem)

    @Query("DELETE FROM cache WHERE expiry < :now")
    suspend fun evictExpired(now: Long)
}

// 离线写入调用
viewModelScope.launch(Dispatchers.IO) {
    cacheDao.upsert(CacheItem(key, value, System.currentTimeMillis() + TTL))
}
  • 写策略:直接映射 SQLite 事务,通过 @Insert/@Update 注解简化。
  • 冲突处理:OnConflictStrategy.REPLACE 或 ABORT。
  • 离线队列:结合 Flow 实现自动重试和数据刷新。

4.4 性能考量与基准数据

以下基准数据来自同一台 Android 中端设备(骁龙 778G,8GB 内存),每条记录 256 字节,1000 次操作取中位数:

操作 SQLite Realm Room(基于 SQLite)
单条插入 ~1.2 ms ~0.4 ms ~1.3 ms
批量插入(100条) ~8.5 ms(事务合并) ~2.1 ms ~9.0 ms(事务合并)
主键查询 ~0.3 ms ~0.2 ms ~0.4 ms
模糊查询(LIKE) ~2.1 ms ~1.5 ms ~2.3 ms
内存峰值(1000条) ~4 MB ~12 MB ~5 MB
数据库文件大小 ~280 KB ~420 KB ~300 KB
关键发现:
  • Realm 在单条和批量写入上优势明显,但内存占用高出 2 – 3 倍。
  • SQLite 与 Room 在查询性能上几乎一致(Room 有微弱 ORM 开销)。
  • 包体积增量:SQLite < Room < Realm,小型项目建议优先 SQLite。

4.5 选型决策框架

综合离线读写需求、性能敏感度、团队技术栈和长期维护成本,推荐按以下流程决策:

本地存储引擎选型对比框架 存储选型需求 SQLite 关系型 · ACID · 标准SQL 跨平台 轻量级 生态成熟 Realm 对象存储 · 高性能 · 自动同步 低延迟 移动优先 实时同步 Room SQLite抽象层 · Google官方 编译校验 LiveData集成 选型决策建议输出 根据离线读写需求、性能要求、开发效率综合选择

4.6 选型建议与工程落地

  • 标准化离线缓存(SQLite):如果团队跨平台(iOS + Android + Desktop)且数据模型稳定,直接使用 SQLite + 手写 SQL 可获得最佳可移植性和最小体积。
  • 高并发离线读写(Realm):如果离线写入频率极高(如 IoT 设备、实时协作),Realm 的延迟写入和对象级监听能显著降低延迟。注意评估授权成本。
  • Android 原生生态(Room):如果项目已使用 MVVM + LiveData / Flow,Room 是零摩擦选择。利用 @Transaction 和 @RawQuery 可弥补批量写性能差距。
性能优化红线:无论选择哪个引擎,离线写入时务必使用批量事务(100 – 500 条/事务)并避免在主线程操作 DB。单条自动事务的开销是批量事务的 5 – 10 倍,在离线回放队列场景下会直接导致 ANR 或卡顿。
// 通用批量事务模板(以 SQLite 为例)
// 每次提交 200 条,避免单条提交的开销
int batchSize = 200;
sqlite3_exec(db, "BEGIN", 0, 0, 0);
for (int i = 0; i < total; i++) {
    bindAndStep(db, items[i]);
    if ((i + 1) % batchSize == 0) {
        sqlite3_exec(db, "COMMIT", 0, 0, 0);
        sqlite3_exec(db, "BEGIN", 0, 0, 0);
    }
}
sqlite3_exec(db, "COMMIT", 0, 0, 0);