引言
本篇记录在 flutter 项目中频繁操作数据库(增删改)遇到的问题以及处理方案。
-
场景描述
小编在业务中有这样一个场景,监听数据流,对上万条数据对同一张表进行插库、改库操作。
按照常规方案,小编使用了 future 对数据库操作进行包裹异步封装。 -
异常 cannot open file、Too many open files、OOM
当数据库操作次数达到一定量级,我们观察到控制台输出异常,甚至直接OOM ,具体日志如下:
E/SQLiteLog(15803): (14) cannot open file at line 32460 of [69906880ce]
Large object allocation failed: ashmem_create_region failed for 'large object space allocation': Too many open files
I/art (14225): Starting a blocking GC Alloc
[log] DatabaseException(Cursor window allocation of 2048 kb failed. ) sql 'SELECT * FROM ...
DatabaseException(Cursor window allocation of 2048 kb failed
Could not allocate CursorWindow '/data/user/0/cn.rex.com/databases/rex_record.db' of size 2097152 due to error -24.
Alloc sticky concurrent mark sweep GC freed 22126(2MB) AllocSpace objects, 1(20KB) LOS objects, 39% free, 9MB/15MB, paused 696us total 17.869ms
Large object allocation failed: ashmem_create_region failed for 'large object space allocation': Too many open files
分析
这是由于大量的 future 同时对数据库进行操作,造成数据库文件 lock,加剧了死锁等待,最终等待的数据量级太大,作为压死骆驼的最后一根稻草。我们的数据库直接崩溃了。
处理方案:对 future 操作使用队列管理
像这种大量频繁的对同一个文件进行操作,我们需要使用 队列 的方式对 future 任务进行限流管理。
- 第一步: 使用 Stream 实现 future 队列管理
封装一个 future 队列工具, 大致代码如下,可直接 copy 使用
import 'dart:async';
/// future 操作队列工具
typedef FutureQueueFunc = Future<dynamic> Function();
class FutureQueueUtil {
final StreamController<_FutureQueueItem> _futureQueue =
StreamController<_FutureQueueItem>(sync: true);
StreamSubscription? _subscription;
void init() {
_onListen();
}
void dispose() {
_cancel();
}
void add(
FutureQueueFunc func, {
Function(dynamic result)? callback,
}) {
_futureQueue.sink.add(
_FutureQueueItem(func, callback: callback),
);
}
void _onListen() {
_cancel();
_subscription = _futureQueue.stream.asyncMap(
(event) async {
final result = await event.func();
if (event.callback != null) {
event.callback!(result);
}
return true;
},
).listen(null);
}
void _cancel() {
_subscription?.cancel();
_subscription = null;
}
}
class _FutureQueueItem {
final FutureQueueFunc func;
final Function(dynamic result)? callback;
_FutureQueueItem(this.func, {this.callback});
}
- 第二步: 在数据库操作的管理类中进行使用,大致结构如下
///打印记录管理
class DatabaseManager {
static DatabaseManager? _instance;
//数据库增删改队列操作 (增删改加入队列)
final FutureQueueUtil _dataOptQueue = FutureQueueUtil();
static DatabaseManager get instance {
_instance ??= DatabaseManager._();
return _instance!;
}
DatabaseManager._() {
//初始化队列监听
_dataOptQueue.init();
}
//新增或修改操作
Future<bool> insertOrUpdate(Map data) async {
Completer<bool> completer = Completer();
_dataOptQueue.add(
() async {
//具体的数据库操作,例如:
// await db.insert;
// return true;
},
callback: (result) {
completer.complete(result);
},
);
return completer.future;
}
//删除操作
Future<bool> remove() async {
Completer<bool> completer = Completer();
_dataOptQueue.add(
() async {
//具体的数据库操作,例如:
// await db.delete;
// return true;
},
callback: (result) {
completer.complete(result);
},
);
return completer.future;
}
}
将每个增删改操作,加入队列进行管理,上一个操作完成后再进行下一个操作,避免文件的死锁等待,同时这种方案也能减轻 flutter cpu 的瞬时消耗,弱化对 UI 线程的影响。
网友评论