如何在Dart 2.15中用Isolates解析大型JSON数据

276 阅读7分钟

解析JSON数据是许多应用程序的一项非常常见的任务。

如果我们的JSON有效载荷很小,那么在主隔离区(也就是我们的应用程序用户界面运行的地方)上运行解析代码是安全的。

但是,如果我们的JSON有效载荷很大,解析它的时间可能会超过帧预算(60Hz刷新率为16毫秒),我们可能会遇到丢帧和UI卡顿。

在这种情况下,最好在一个工作者隔离区中运行所有的解析代码,该隔离区主隔离区 同时运行,不会影响UI。

因此,在这篇文章中,我们将学习如何用两种方式解析JSON。

  • 使用compute() 函数
  • 通过催生一个隔离器来完成工作,并在完成后调用Isolate.exit()

我们将主要关注在Dart中解析JSON的代码,但我也将分享一个Flutter应用程序的例子,以便我们可以看到所有东西是如何结合在一起的。如果你感到好奇,这里是在Dartpad上运行的。

用隔离物进行JSON解析。在Dartpad上查看

[

赞助者

与Andrea一起编程对每个人都是免费的。帮助我保持这种方式,看看这个赞助商。

Open-Source Backend Server for Flutter Developers

**面向Flutter开发者的开源后端服务器。**Appwrite是一个安全的、自我托管的解决方案,它为开发者提供了一套易于使用的REST API来管理他们的核心后端需求。你可以用Appwrite构建任何东西!点击这里了解更多。

](appwrite.io/?utm_source…)

如何在Dart中解析JSON数据

我们的目标是解析一个JSON文件,其中包含我网站上所有教程的列表。这里有一个快照供参考。

{
  "results": [
    {
      "title": "Flutter Tutorial: Stopwatch App with Custom UI and Animations",
      "url": "https://codewithandrea.com/videos/stopwatch-flutter-ui-tutorial/",
      "date": "Dec 6, 2021",
      "contentType": "video"
    },
    {
      "title": "Responsive layouts in Flutter: Split View and Drawer Navigation",
      "url": "https://codewithandrea.com/articles/flutter-responsive-layouts-split-view-drawer-navigation/",
      "date": "Nov 26, 2021",
      "contentType": "article"
    },
    {
      "title": "How to create a Flutter GridView with content-sized items",
      "url": "https://codewithandrea.com/articles/flutter-layout-grid-content-sized-items/",
      "date": "Nov 24, 2021",
      "contentType": "article"
    }
  ]
}

这个例子只显示了3个项目,但整个文件包含140个项目,重量为28KB。

因此,让我们先写一个类型安全的SearchResult 类,我们将用它来解析这些数据。

class SearchResult {
  SearchResult({
    required this.title,
    required this.url,
    required this.date,
  });
  final String title;
  final String url;
  final String date;

  factory SearchResult.fromJson(Map<String, dynamic> data) {
    return SearchResult(
      title: data['title'],
      url: data['url'],
      date: data['date'],
    );
  }
}

如果你对上面的工厂构造函数语法不熟悉,请查看我的Dart/Flutter中解析JSON的基本指南

用隔离物解析JSON

现在是有趣的部分。

让我们先创建一个SearchResultsParser 类,它可以将编码后的JSON字符串解析为List<SearchResult>

import 'dart:convert';

class SearchResultsParser {
  List<SearchResult> _decodeAndParseJson(String encodedJson) {
    final jsonData = jsonDecode(encodedJson);
    final resultsJson = jsonData['results'] as List<dynamic>;
    return resultsJson.map((json) => SearchResult.fromJson(json)).toList();
  }
}

请注意,_decodeAndParseJson() 方法不是异步的,但如果结果的数量很大,它仍然需要很长的时间来执行。

正如我们所说,我们想在后台解析编码后的JSON。

这可以通过添加一个新的parseInBackground() 方法轻松实现,该方法使用compute() 函数。

import 'dart:convert';
import 'package:flutter/foundation.dart';

class SearchResultsParser {
  Future<List<SearchResult>> parseInBackground(String encodedJson) {
    // compute spawns an isolate, runs a callback on that isolate, and returns a Future with the result
    return compute(_decodeAndParseJson, encodedJson);
  }

  List<SearchResult> _decodeAndParseJson(String encodedJson) {
    final jsonData = jsonDecode(encodedJson);
    final resultsJson = jsonData['results'] as List<dynamic>;
    return resultsJson.map((json) => SearchResult.fromJson(json)).toList();
  }
}

这就是了!

通过使用compute() ,我们可以将我们的解析代码移到工作者隔离区,并获得我们想要的所有性能优势。

但是,这里面还有更多的内容吗?

使用Isolate.exit()的快速并发性

事实证明,在Flutter 2.8(Dart 2.15)中,compute() 函数已被更新,以利用Isolate.exit() ,这个新方法可用于在恒定时间内将结果传回主隔离区。

如果你想在工作者隔离区中运行一个任务,并将结果返回给主隔离区,那么你只需要compute() 这个函数。

但我认为,学习如何使用隔离区的API来做同样的事情是很有趣的。

因此,这里是我们的SearchResultsParser 类的另一种实现。

class SearchResultsParser {
  // 1. pass the encoded json as a constructor argument
  SearchResultsParser(this.encodedJson);
  final String encodedJson;

  // 2. public method that does the parsing in the background
  Future<List<SearchResult>> parseInBackground() async {
    // create a port
    final p = ReceivePort();
    // spawn the isolate and wait for it to complete
    await Isolate.spawn(_decodeAndParseJson, p.sendPort);
    // get and return the result data
    return await p.first;
  }

  // 3. json parsing
  Future<void> _decodeAndParseJson(SendPort p) async {
    // decode and parse the json
    final jsonData = jsonDecode(encodedJson);
    final resultsJson = jsonData['results'] as List<dynamic>;
    final results = resultsJson.map((json) => SearchResult.fromJson(json)).toList();
    // return the result data via Isolate.exit()
    Isolate.exit(p, results);
  }
}

请注意,其中有几个不同之处。

  • 我们在parseInBackground() 方法中明确地用Isolate.spawn() 创建一个新的隔离体,并等待它完成
  • 我们对_decodeAndParseJson() 中的json进行解码和解析,并通过以下方式返回结果Isolate.exit()
  • 传递给Isolate.spawn()_decodeAndParseJson() 方法只能接受一个SendPort 参数,因此我们必须将encodedJson 作为实例变量来访问,该变量由构造函数设置。

再一次,在这个用例中你不需要Isolate.spawn()Isolate.exit() ,因为compute() 函数用更少的代码完成了同样的工作。

一个JSON文件应该有多大才能在后台进行解析?

它应该是10KB吗?100KB?1MB?更大?

虽然我没有一个准确的答案,但我们可以用一个科学的方法。

那就是。

  • 我们应该在一个低端设备上以配置文件模式运行该应用程序
  • 测试不同的有效载荷大小,看看我们是否在主要的隔离物上得到跳过的帧。

如果这听起来工作量太大,我们可以保持安全的一面,在后台对10KB以上的有效载荷进行解析。毕竟,这只需一行代码就能完成。

compute(_decodeAndParseJson, encodedJson)

网络代码应该在工作者隔离上运行吗?

到目前为止,我们只把JSON解析代码移到了worker isolate上。

网络代码呢?考虑一下这个APIClient 类。

import 'package:http/http.dart' as http;

class APIClient {
  Future<List<SearchResult>> downloadAndParseJson() async {
    // get the data from the network
    final response = await http
        .get(Uri.parse('https://codewithandrea.com/search/search.json'));
    if (response.statusCode == 200) {
      // on success, parse the JSON in the response body
      final parser = SearchResultsParser();
      return parser.parseInBackground(response.body);
    } else {
      // on failure, throw an exception
      throw Exception('Failed to load json');
    }
  }
}

这段代码使用http包从网络上获取数据,然后将解析逻辑委托给我们所创建的SearchResultsParser 类。

但是,http.get() 的调用不是也应该在worker isolate上运行吗?

事实证明,网络和文件IO运行在一个单独的任务运行器上。而当IO操作完成后,其结果会被传回主隔离区。

换句话说:我们可以安全地使用基于Future的API来执行IO,而Dart会在内部做正确的事情。

无阻塞等待(使用异步代码)和并行运行代码(使用隔离)之间存在着根本的区别。这个关于异步与隔离的视频很好地解释了这一点。

Flutter应用实例

现在我们已经处理了JSON解析和网络代码,我们可以创建一个简单的Flutter应用程序,显示所有的搜索结果。

这个应用使用Riverpod包将所有东西联系在一起,但任何其他的依赖注入包也可以使用。

这就是它,在Dartpad上运行。

带有隔离物的JSON解析。在Dartpad上查看

结论

在处理大的JSON数据时,我们应该将解析代码卸载到一个工作者隔离区,最简单的方法是使用compute() 函数。

从Flutter 2.8开始,由于Dart 2.15引入的快速并发特性,compute() 函数可以在恒定时间内将结果数据返回给主隔离区

参考资料

如果您想了解更多关于隔离器的信息,请查看官方文档中的这一页。

Flutter团队也有一些关于这个主题的很好的视频。

以下是Flutter 2.8和Dart 2.15的公告。

而这个StackOverflow线程解释了Dart如何在引擎盖下管理IO操作。

编码愉快!