Flutter 3.7 的 background isolate 绝对是一大惊喜,尽管它在 release note 里被一笔带过 ,可是某种程度上它能够说是 3.7 里最实用的存在:由于运用简略,提高又直观

Background isolate YYDS

前言

咱们知道 Dart 里能够经过新建 isolate 来履行”真“异步使命,而本身咱们的 Dart 代码也是运转在一个独立的 isolate 里(简称 root isolate),而 isolate 之间不同享内存,只能经过音讯传递在 isolates 之间交换状况。

所以 Dart 里不像 Java 相同需要线程锁。

而在 Dart 2.15 里新增了 isolate groups 的概念,isolate groups 中的 isolate 同享程序里的各种内部数据结构,也便是尽管 isolate groups 仍是不允许 isolate 之间同享可变目标,但 groups 能够经过同享堆来完成结构同享,例如:

Dart 2.15 后能够将目标直接从一个 isolate 传递到另一 isolate,而在此之前只支持根底数据类型。

那么假如运用场景来到 Flutter Plugin ,在 Flutter 3.7 之前,咱们只能从 root isolate 去调用 Platform Channels ,假如你尝试从其他 isolate 去调用 Platform Channels ,就会收成这样的过错正告:

Flutter 小技巧之 3.7 性能优化 background isolate

例如,在 Flutter 3.7 之前,Platform Channels 是和 _DefaultBinaryMessenger 这个大局目标进行通信,可是一但切换了 isolate ,它就会变为 null ,由于 isolate 之间不同享内存。

而从 Flutter 3.7 开始,简略地说,Flutter 会经过新增的 BinaryMessenger 来完成非 root isolate 也能够和 Platform Channels 直接通信,例如:

咱们能够在全新的 isolate 里,经过 Platform Channels 获取到渠道上的原始图片后,在这个独立的 isolate 进行一些数据处理,然后再把数据返回给 root isolate ,这样数据处理逻辑既能够完成跨渠道通用,又不会卡顿 root isolate 的运转。

Background isolate

现在 Flutter 在 Flutter 3.7 里引入了 RootIsolateTokenBackgroundIsolateBinaryMessenger 两个目标,当 background isolate 调用 Platform Channels 时, background isolate 需要和 root isolate 树立相关,所以在 API 运用上大约会是如下代码所示:

RootIsolateToken rootIsolateToken =
    RootIsolateToken.instance!;
Isolate.spawn((rootIsolateToken) {
  doFind2(rootIsolateToken);
}, rootIsolateToken);
doFind2(RootIsolateToken rootIsolateToken) {
  // Register the background isolate with the root isolate.
  BackgroundIsolateBinaryMessenger
      .ensureInitialized(rootIsolateToken);
  //......
}

经过 RootIsolateToken 的单例,咱们能够获取到当前 root isolate 的 Token ,然后在调用 Platform Channels 之前经过 ensureInitialized 将 background isolate 需要和 root isolate 树立相关。

大约便是 token 会被注册到 DartPluginRegistrant 里,然后 BinaryMessenger_findBinaryMessenger 时会经过 BackgroundIsolateBinaryMessenger.instance 发送到对应的 listener

完好代码如下所示,逻辑也很简略,便是在 root isolate 里获取 RootIsolateToken ,然后在调用 Platform Channels 之前 ensureInitialized 相关 Token 。

 InkWell(
   onTap: () {
     ///获取 Token 
     RootIsolateToken rootIsolateToken =
         RootIsolateToken.instance!;
     Isolate.spawn(doFind, rootIsolateToken);
   },
////////////////
doFind(rootIsolateToken) async {
  /// 注册 root isolaote
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
  ///获取 sharedPreferencesSet 的  isDebug 标识位
  final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
      .then((sharedPreferences) => sharedPreferences.setBool('isDebug', true));
  /// 获取本地目录
  final Future<Directory> tempDirFuture = path_provider.getTemporaryDirectory();
  /// 合并履行
  var values = await Future.wait([sharedPreferencesSet, tempDirFuture]);
  final Directory? tempDir = values[1] as Directory?;
  final String dbPath = path.join(tempDir!.path, 'database.db');
  File file = File(dbPath);
  if (file.existsSync()) {
    ///读取文件
    RandomAccessFile reader = file.openSync();
    List<int> buffer = List.filled(256, 0);
    while (reader.readIntoSync(buffer) == 256) {
      List<int> foo = buffer.takeWhile((value) => value != 0).toList();
      ///读取成果
      String string = utf8.decode(foo);
      print("######### $string");
    }
    reader.closeSync();
  }
}

这里之所以能够在 isolate 里直接传递 RootIsolateToken ,便是得益于前面所说的 Dart 2.15 的 isolate groups

其实入下代码所示,上面的完成换成 compute 也能够正常履行,当然,假如是 compute 的话,有一些比较特殊情况需要注意

RootIsolateToken rootIsolateToken =    RootIsolateToken.instance!;
compute(doFind, rootIsolateToken);

如下代码所示, doFind2 方法在 doFind 的根底上,将 Future.waitawait 修改为 .then 去履行,假如这时候你再调用 spawncompute ,你就会发现 spawn 下代码仍然能够正常履行,可是 compute 却不再正常履行

onTap: () {
  RootIsolateToken rootIsolateToken =
      RootIsolateToken.instance!;
  compute(doFind2, rootIsolateToken);
},
onTap: () {
  RootIsolateToken rootIsolateToken =
      RootIsolateToken.instance!;
  Isolate.spawn(doFind2, rootIsolateToken);
},
doFind2(rootIsolateToken) async {
  /// 注册 root isolaote
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
  ///获取 sharedPreferencesSet 的  isDebug 标识位
  final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
      .then((sharedPreferences) => sharedPreferences.setBool('isDebug', true));
  /// 获取本地目录
  final Future<Directory> tempDirFuture = path_provider.getTemporaryDirectory();
  ///////////////////// Change Here //////////////////
  /// 合并履行
  Future.wait([sharedPreferencesSet, tempDirFuture]).then((values) {
    final Directory? tempDir = values[1] as Directory?;
    final String dbPath = path.join(tempDir!.path, 'database.db');
    ///读取文件
    File file = File(dbPath);
    if (file.existsSync()) {
      RandomAccessFile reader = file.openSync();
      List<int> buffer = List.filled(256, 0);
      while (reader.readIntoSync(buffer) == 256) {
        List<int> foo = buffer.takeWhile((value) => value != 0).toList();
        String string = utf8.decode(foo);
        print("######### $string");
      }
      reader.closeSync();
    }
  }).catchError((e) {
    print(e);
  });
}

为什么会这样?compute 不便是 Flutter 针对 Isolate.spawn 的简易封装吗?

其实原因就在这个封装上,compute 现在不是直接履行 Isolate.spawn 代码,而是履行 Isolate.run ,而 Isolate.run 针对 Isolate.spawn 做了一些特殊封装。

compute 内部会将履行目标封装成 _RemoteRunner 再交给 Isolate.spawn 履行,而 _RemoteRunner 在履行时,会在最终强制调用 Isolate.exit ,这就会导致前面的 Future.wait 还没履行,而 Isolate 就退出了,从而导致代码无效的原因。

Flutter 小技巧之 3.7 性能优化 background isolate

Flutter 小技巧之 3.7 性能优化 background isolate

另外在 Flutter 3.7 上 ,假如 background isolate 调用 Platform Channels 没有相关 root isolate,也能看到过错提示你初始化相关,所以这也是为什么我说它运用起来很简略的原因。

Flutter 小技巧之 3.7 性能优化 background isolate

除此之外,最近刚好遇到有“机智”的小伙伴说 background isolate 无法正常调用,看了下代码是把 RootIsolateToken.instance!; 写到了 background isolate 履行的方法里。

Flutter 小技巧之 3.7 性能优化 background isolate

你猜假如这样有效,为什么官方不直接把这个获取写死在 framewok?

其实这也是 isolates 常常引起歧义的原因,isolates 是隔离,内存不同享数据,所以 root isolate 里的 RootIsolateToken 在 background isolate 里直接获肯定是 null ,所以这也是 isolate 运用时需要分外注意的一些小细节。

另外还有如 #36983 等问题,也推动了前面所说的 compute 相关的更改。

最终,假如需要一个完好 Demo 的话,能够参阅官方的 background_isolate_channels ,项目里首要经过 SimpleDatabase_SimpleDatabaseServer 的交互,来模拟展示 root isolate 和 background isolate 的调用完成。

最终

总的来说 background isolate 并不难理解,自从 2018 年在 issue #13937 被提出之后就饱尝重视,乃至官方还主张过咱们经过 ffi 另辟蹊径去完成,其时的 issue 也被搭上了 P5 的 Tag。

信任咱们都知道 P5 意味着什么。

所以 background isolate 能在 Flutter 3.7 看到是相当难得的,当然这也离不开 Dart 的日益老练的支持,一起 background isolate 也给咱们带来了更多的可能性,其中最直观便是功能优化上多了新的可能,代码写起来也变得更顺利。

期待 Flutter 和 Dart 在后续的版本中还能给咱们带来更多的惊喜。