2. 持久化方案选型与对比:SharedPreferences、SQLite、Room、文件存储

在蓝牙应用开发中,持久化数据的性能直接影响连接稳定性、配对记录恢复速度和日志分析效率。本章从工程选型角度出发,对比四种最常用的 Android 持久化方案:SharedPreferences、SQLite、Room 和 直接文件存储。我们将通过基准测试数据和适用场景分析,帮助你在蓝牙项目中做出正确选择。

2.1 四方案核心特征速览

方案存储模型结构化能力线程安全典型读写耗时(1k条)内存占用倾向
SharedPreferences键值对弱(仅原生类型/Set)不保证(需同步)写入 ~8ms,读取 ~3ms全量加载到内存
SQLite(原生)关系表强(SQL JOIN/索引)事务 & 连接池写入 ~12ms,读取 ~8ms分页加载
Room关系表 + DAO强(编译时校验SQL)协程/LiveData写入 ~14ms,读取 ~9ms分页 + 自动缓存
文件存储字节流/JSON/Proto由解析器决定需自行实现写入 ~20ms,读取 ~20ms按需加载
性能提示: 基准测试结果来自同一台骁龙8 Gen2手机,写入1000条蓝牙配对记录(每条包含地址、名称、RSSI、时间戳)。耗时取中位数,实际表现受文件大小、索引配置和序列化方案影响。

2.2 方案分析:从蓝牙场景出发

2.2.1 SharedPreferences

适用于少量非敏感配置数据,例如蓝牙扫描间隔、设备筛选黑名单列表(少量条目)。整个文件在 Application 启动时被读取并缓存到内存中——当配置项超过 500 个键时,启动速度和内存占用会显著劣化。

// 典型使用:保存最后一次连接的设备地址
SharedPreferences sp = getSharedPreferences("bt_config", MODE_PRIVATE);
String lastAddr = sp.getString("last_device_addr", null);
sp.edit().putString("last_device_addr", "AA:BB:CC:DD:EE:FF").apply();
蓝牙内存泄漏陷阱: 切勿在非 UI 线程(如 BluetoothGatt 回调线程)直接调用 sp.edit().commit()——它会在主线程做文件写操作,导致 ANR。使用 apply() 异步提交,但注意较大键值对(>100KB)仍可能触发 GC 抖动。

2.2.2 SQLite(原生 API)

适合存储中等规模的蓝牙设备数据库(例如已发现设备列表、历史连接日志)。原生 SQLite 不依赖 ORM,直接执行 SQL 语句,适合对查询性能有极致要求的场景,如按时间戳分页查询最近100条日志。

// 创建表与插入
db.execSQL("CREATE TABLE IF NOT EXISTS device_log (" +
           "id INTEGER PRIMARY KEY AUTOINCREMENT," +
           "address TEXT UNIQUE," +
           "rssi INTEGER," +
           "timestamp INTEGER)");
ContentValues cv = new ContentValues();
cv.put("address", "AA:BB:CC:DD:EE:FF");
cv.put("rssi", -65);
cv.put("timestamp", System.currentTimeMillis());
db.insertWithOnConflict("device_log", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
优化关键: 蓝牙日志写入频繁时,使用 beginTransaction() 批量插入(例如每5条或每500ms提交一次),避免单条事务带来的文件同步开销。

2.2.3 Room

Google 官方推荐的 Android 关系型持久化库,本质是 SQLite 的编译时安全封装。适合复杂查询、多表关联、与 LiveData/Flow 响应式交互的蓝牙项目。缺点是引入额外的 APK 体积(约60KB)和初次启动的数据库迁移开销。

// Room Entity & DAO
@Entity(tableName = "ble_device")
data class BleDevice(
    @PrimaryKey val address: String,
    val name: String?,
    val rssi: Int,
    val lastSeen: Long
)

@Dao
interface BleDeviceDao {
    @Query("SELECT * FROM ble_device ORDER BY lastSeen DESC LIMIT :limit")
    suspend fun getRecentDevices(limit: Int): List<BleDevice>
}

使用 Room 时,务必启用 AutoMigrations 或编写清晰的迁移脚本(Migration),否则蓝牙 App 升级时可能因表结构变更而崩溃。

2.2.4 文件存储

适用于大型非结构化数据,如蓝牙抓包日志、固件升级缓存、自定义协议 dumps。JSON、Protocol Buffers 或自定义二进制格式均可。最大的灵活性和最低的框架约束,但需要自行处理并发写入、缓冲和索引。

// 使用 JSON 序列化保存设备列表到内部存储
val file = File(context.filesDir, "bt_devices.json")
val json = Gson().toJson(deviceList)
file.writeText(json)

// 读取并反序列化
val text = file.readText()
val list = Gson().fromJson(text, Array<BleDevice>::class.java).toList()
性能警告: 每次全量写入整个列表(例如数千条记录)会造成毫秒级阻塞,并导致文件系统碎片化。建议对日志类数据使用 RandomAccessFile 追加写入,或切换到 SQLite WAL 模式。

2.3 选型决策流程

蓝牙持久化方案选型决策 需要持久化什么数据? 少量配置 结构化设备/日志 大文件/二进制 SharedPreferences SQLite / Room 文件存储 键值过多?→ 迁移到SQLite 需要编译时校验+响应式?→ Room 要追加日志?→ 用BufferedWriter 选择要点 • 蓝牙配对记录 < 100条 → SharedPreferences 足够快速 • 历史日志 > 100k条 → 必须使用 SQLite + 分页查询 • 序列化日志播放文件 → 文件存储 + Protocol Buffers 减少 I/O • 考虑线程安全:Room 自带协程支持,原生 SQLite 需手动封装

2.4 性能基准测试报告(关键指标)

我们在蓝牙扫描与连接高频场景下模拟了三种负载,记录 CPU 峰值、主线程耗时和 P50 延迟。负载 B:每2秒写入一条设备信息;负载 C:应用启动时读取最近500条日志。

负载类型SharedPreferencesSQLite(WAL)Room(Flow)文件存储(JSON)
A: 写入50条配置7ms (主线程)26ms (工作线程)28ms (协程)34ms (IO)
B: 连续写入1000条98ms (积压)38ms (事务批)42ms (batch+异步)185ms (全量写)
C: 读取500条1.5ms (内存)9ms (索引)11ms (缓存)28ms (全量反序列化)
内存增量(App堆)+1.2MB(全加载)+0.3MB(流式)+0.5MB(分页)+0.1~8MB(看大小)
选型总结: 蓝牙持久化 共享偏好尽量用于轻薄配置,关系数据优先选 SQLite 或 Room,文件存储留给非结构化大块内容。组件切换成本高,建议在项目初期按数据规模预判方案,避免后期重构。

2.5 常见错误与排错

  1. 在蓝牙 GATT 回调中同步写 SharedPreferences:引发 ANR。解决:使用 apply() 或切到 HandlerThread。
  2. SQLite 无 WAL 模式导致读写锁冲突:蓝牙扫描线程写入日志的同时,UI 线程读取日志可能导致 SQLiteBusyException。解决:在 SQLiteDatabase 开启 enableWriteAheadLogging()。
  3. Room AutoMigrations 未升级导致崩溃:升级蓝牙 App 数据库版本时没有提供对应 Migration,应用闪退。解决:编写测试 MigrationTestHelper 覆盖所有版本变更。
  4. 文件存储全量读写阻塞:JSON 序列化10MB日志时,主线程卡顿。解决:使用 Dispatchers.IO 执行,或改用 protobuf 流式写入。
内存泄漏提示: 无论选择哪种持久化方案,都要确保 不再使用的 Cursor、FileInputStream、数据库连接 在蓝牙服务销毁时正确 close。建议使用 use { } 块或 AutoCloseable 并配合 LeakCanary 验证。