Flutter调用await database后对象列表属性异常清空问题
问题根因
- 该现象是sqflite插件的典型Debug模式踩坑场景,仅在首次异步初始化数据库的间隙触发,Release构建通常不会复现,和本身的数据库表结构、存储逻辑无关。
- 核心触发逻辑:
- 字符串、数字这类基础类型是值语义,在异步事件循环的流转过程中不会被意外修改,因此
w.name的输出始终正常。 - 列表是引用语义,当代码执行到
await database时,当前执行流会被挂起,事件循环会转而处理底层平台通道的消息编解码任务。sqflite 2.0.0~2.0.5区间的版本在Debug模式下存在编解码临时缓冲区复用的逻辑,会把未做深拷贝、处于消息传递路径上的可变列表的底层存储直接清空,最终出现列表长度从2变为0的现象。 - 其他数据库交互逻辑正常,是因为这类场景下数据库已经初始化完成,
await database会直接返回已缓存的_database实例,没有执行流挂起的间隙,自然不会触发上述清空逻辑。
- 字符串、数字这类基础类型是值语义,在异步事件循环的流转过程中不会被意外修改,因此
- 可以排除
toMap()方法的干扰:按照代码执行顺序,toMap()是在await database完成之后才会被调用,而列表长度的变化发生在await database的执行间隙,此时还未走到Map构建的逻辑,因此不存在方法内部清空列表的可能。
可落地的解决方案
- 优先修复插件版本问题
- 将sqflite插件升级到最新稳定版本,上述Debug模式的编解码bug在后续版本已经完全修复,不会出现可变列表被意外清空的问题。
- 调整数据库初始化时机,把首次数据库初始化的逻辑提前到应用启动阶段执行,不要等第一次存储训练记录时才触发
initDB流程。这样调用saveWorkout时,database已经是可用的缓存实例,await database会同步直接返回结果,不会产生执行流挂起的间隙,从流程上规避问题触发条件。
- 代码层面的稳妥规避(适合暂时无法升级插件的场景)
- 在进入异步等待流程前,先对Workout对象的可变列表属性做一次深拷贝,切断原列表的引用关联,避免后续异步流程的其他逻辑意外修改原引用。
- 参考修改后的代码示例:
Future<Workout> saveWorkout(Workout w) async{ // 提前深拷贝可变列表,和原引用做切割 final copiedExerciseList = List.from(w.exercises); print("length before await: " + copiedExerciseList.length.toString()); print(w.name); var dbClient = await database; // 后续使用拷贝完成的列表做判断/存储 print("length after await: " + copiedExerciseList.length.toString()); print(w.name); final dataMap = w.toMap(); dataMap["exercises"] = copiedExerciseList; w.id = await dbClient.insert("Workout", dataMap); return w; }- 注意不要在跨异步边界的代码中直接依赖外部传入的可变对象引用,Debug模式下平台通道的编解码时机、GC时机都是不确定的,很容易出现引用被意外修改的问题。
内容的提问来源于stack exchange,提问作者A.Mat
相关产品推荐
相关产品推荐

