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 选型决策流程
2.4 性能基准测试报告(关键指标)
我们在蓝牙扫描与连接高频场景下模拟了三种负载,记录 CPU 峰值、主线程耗时和 P50 延迟。负载 B:每2秒写入一条设备信息;负载 C:应用启动时读取最近500条日志。
| 负载类型 | SharedPreferences | SQLite(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 常见错误与排错
- 在蓝牙 GATT 回调中同步写 SharedPreferences:引发 ANR。解决:使用
apply()或切到 HandlerThread。 - SQLite 无 WAL 模式导致读写锁冲突:蓝牙扫描线程写入日志的同时,UI 线程读取日志可能导致
SQLiteBusyException。解决:在SQLiteDatabase开启enableWriteAheadLogging()。 - Room AutoMigrations 未升级导致崩溃:升级蓝牙 App 数据库版本时没有提供对应 Migration,应用闪退。解决:编写测试
MigrationTestHelper覆盖所有版本变更。 - 文件存储全量读写阻塞:JSON 序列化10MB日志时,主线程卡顿。解决:使用
Dispatchers.IO执行,或改用 protobuf 流式写入。
内存泄漏提示: 无论选择哪种持久化方案,都要确保 不再使用的 Cursor、FileInputStream、数据库连接 在蓝牙服务销毁时正确 close。建议使用
use { } 块或 AutoCloseable 并配合 LeakCanary 验证。