【问题标题】:Flutter: avoid UI freeze when massive Database operation is in progressFlutter:在进行大量数据库操作时避免 UI 冻结
【发布时间】:2026-01-19 09:35:02
【问题描述】:

更新(2020 年 7 月 15 日)

mFeinstein 的回应,目前,是给我第一个可接受的解决方案的唯一答案。


问题

我不得不问你做我想做的事情的最佳方法是什么:

  1. 以异步模式调用 Web 服务
  2. 解析响应
  3. 执行海量数据库操作

所有这些都没有冻结进度动画,比如不确定的进度条。

第一点和第二点没有问题。问题出现在第三个地方,当大量数据库插入正在运行时。而且我还不明白实现这些东西的正确方法是什么。

一些用于澄清的伪代码

UI(显示对话框并运行进度条...)

void callWS() async {
    MyProgressDialog _dialog = DialogHelper.showMyProgressDialog(_context, "Data", "Loading...");
    await getDataFromService();
    _dialog.close();
  }

CONNECTION(进度条上不会出现冻结)

   static Future<void> getDataFromService() async {
    String uri = MY_URI;
    String wsMethod = MY_WS_METHOD;
    String wsContract = MY_WS_CONTRACT;

    SoapObject myRequest = SoapObject.fromSoapObject(namespace: my_namespace, name: wsMethod);

    MyConnectionResult response = await _openMyConnection(myRequest, uri, wsContract, wsMethod);
    if (response.result == MyResultEnum.OK) {
      await _parseResponse(response.data);
    }
  }

DATABASE(在进度条上发生冻结)

  static Future<void> _parseResponse(xml.XmlElement elements) async {
    Database db = await MyDatabaseHelper.openConnection();
    db.transaction((tx) async {
      Batch batch = tx.batch();
      for (xml.XmlElement oi in elements.children) {
        int id = int.parse(oi.findElements("ID").first.text);
        String name = oi.findElements("NAME").first.text;

        DatabaseHelper.insertElement(
          tx,
          id: id,
          name: name,
        );
      }
      batch.commit(noResult: true);
    });
  }

不能替代

我也看到了“计算”函数方法,但是当我调用 db 操作时,sqflite package 似乎有问题。例如:

  static Future<void> performDelete() async {
    Database db = await openMyConnection();
    compute(_performDeleteCompute, db);
  }

  static void _performDeleteCompute(Database db) async {
    db.rawQuery("DELETE MYTABLE");
  }

Console error:'
-> Unhandled Exception: Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized. 
-> If you are running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization),
then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
-> error defaultBinaryMessenger.<anonymous closure> (package:flutter/src/services/binary_messenger.dart:76:7)
    #1      defaultBinaryMessenger (package:flutter/src/services/binary_messenger.dart:89:4)
    #2      MethodChannel.binaryMessenger (package:flutter/src/services/platform_channel.dart:140:62)
    #3      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:146:35)
    #4      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:329:12)
    #5      invokeMethod (package:sqflite/src/sqflite_impl.dart:17:13)
    #6      SqfliteDatabaseFactoryImpl.invokeMethod (package:sqflite/src/factory_impl.dart:31:7)
    #7      SqfliteDatabaseMixin.invokeMethod (package:sqflite_common/src/database_mixin.dart:287:15)
    #8      SqfliteDatabaseMixin.safeInvokeMethod.<anonymous closure> (package:sqflite_common/src/database_mixin.dart:208:43)
    #9      wrapDatabaseException (package:sqflite/src/exception_impl.dart:7:32)
    #10     SqfliteDatabaseFactoryImpl.wrapDatabaseException (package:sqflite/src/factory_impl.dart:27:7)
    #11     SqfliteDatabaseMixin.safeInvokeMethod (package:sqflite_common/src/database_mixin.dart:208:15)
    #12     SqfliteDatabaseMixin.txnRawQuery.<anonymous closure> (package:sqflite_common/src/database_mixin.dart:394:36)
    #13     SqfliteDatabaseMixin.txnSynchronized.<anonymous closure> (package:sqflite_common/src/database_mixin.dart:327:22)
    #14     BasicLock.synchronized (package:synchronized/src/basic_lock.dart:32:26)
    #15     SqfliteDatabaseMixin.txnSynchronized (package:sqflite_common/src/database_mixin.dart:323:33)
    #16     SqfliteDatabaseMixin.txnRawQuery (package:sqflite_common/src/database_mixin.dart:393:12)
    #17     SqfliteDatabaseExecutorMixin._rawQuery (package:sqflite_common/src/database_mixin.dart:126:15)
    #18     SqfliteDatabaseExecutorMixin.rawQuery (package:sqflite_common/src/database_mixin.dart:120:12)
    #19     DatabaseHelper._performDeleteCompute(package:flutter_infocad/Database/DatabaseHelper.dart:368:8)'

并且按照错误日志中的建议,在 runApp() 中首先显式调用 WidgetsFlutterBinding.ensureInitialized(),但没有任何反应。

【问题讨论】:

  • 您是否尝试过在不等待的情况下链接调用? like db.openConnection().then() 请从WS调用一路尝试
  • 我会试一试,但是,“then”实现回调,“await”是一种“等待结束然后继续”。在第一种情况下,进度条很快到达终点,它不会等待。在第二种情况下,所有操作都以串行方式执行,等待每个操作的返回值。假设您有 10 个操作。首先显示进度条。然后等待10个结论。以隐藏进度条结束。相反,在第二种情况下,您必须显示进度条,通过回调并行启动 10 个操作。跟踪每一个回调结果,当所有操作返回时,隐藏进度条
  • 但是,我认为问题是在主线程上运行的大量操作。这就是为什么我尝试使用“计算”​​功能,但没有成功。
  • 请检查这个文档medium.com/flutter-community/… 它说等待可能会阻塞主线程。最好隐含回调
  • 这也可能有助于blog.usejournal.com/…

标签: sqlite flutter dart-async dart-isolates


【解决方案1】:

问题是 Flutter 是单线程的,所以一旦你运行了一个繁重的进程,你的单线程就会阻塞其他任何东西。

解决方案是明智地使用该单线程。

Dart 将有一个事件队列,其中有一堆 Futures 等待处理。一旦 Dart 引擎看到 await,它将让另一个 Future 抓住单线程并让它运行。这样,一个Future 将在Isolate 内一次运行。

所以如果我们变得聪明,我们让每个人都在自己的时间玩,换句话说,我们将我们的任务分解成更小的任务,这样 Dart 引擎就不会饿死其他 Futures 和所有进程等待运行可以有他们的时间。

您的代码的等效项是这样的(假设 for 是需要大量时间来执行的,因为集合很大,而不是单独的步骤):

static Future<void> _parseResponse(xml.XmlElement elements) async {
  Database db = await MyDatabaseHelper.openConnection();
  db.transaction((tx) async {
    Batch batch = tx.batch();
    for (xml.XmlElement oi in elements.children) {      
      await Future(() {
        int id = int.parse(oi.findElements("ID").first.text);
        String name = oi.findElements("NAME").first.text;

         DatabaseHelper.insertElement(
          tx,
          id: id,
          name: name,
         );
      );
    }

    batch.commit(noResult: true);
  });
}

这会将for 循环的每个步骤分割成一个Future,因此在每个步骤中,您的 UI 将有机会执行它需要执行的任何操作以保持动画流畅。请记住,尽管这将产生减慢_parseResponse 的副作用,因为将每个for 步骤放入Future 事件队列将产生额外的成本,因此您可能希望针对您的特定用例进一步优化它.

【讨论】:

    【解决方案2】:

    隔离和计算有时无法与 3rd 方库一起使用,您需要使用 flutter_isolate

    FlutterIsolate 允许在 Flutter 中创建一个 Isolate 使用颤振插件

    【讨论】:

    • 不是一个可靠的解决方案,“这个插件没有经过大量插件的测试,只有一小部分我一直在使用,比如flutter_notification、flutter_blue和flutter_startup。”
    【解决方案3】:

    这个任务适用于原生 ios 和 android 代码,你有真正的多线程。实现解析和插入应该不会花很长时间。

    【讨论】: