解析JSON数据是许多应用程序的一项非常常见的任务。
如果我们的JSON有效载荷很小,那么在主隔离区(也就是我们的应用程序用户界面运行的地方)上运行解析代码是安全的。
但是,如果我们的JSON有效载荷很大,解析它的时间可能会超过帧预算(60Hz刷新率为16毫秒),我们可能会遇到丢帧和UI卡顿。
在这种情况下,最好在一个工作者隔离区中运行所有的解析代码,该隔离区与主隔离区 同时运行,不会影响UI。
因此,在这篇文章中,我们将学习如何用两种方式解析JSON。
- 使用
compute()函数 - 通过催生一个隔离器来完成工作,并在完成后调用
Isolate.exit()。
我们将主要关注在Dart中解析JSON的代码,但我也将分享一个Flutter应用程序的例子,以便我们可以看到所有东西是如何结合在一起的。如果你感到好奇,这里是在Dartpad上运行的。
用隔离物进行JSON解析。在Dartpad上查看。
[
赞助者
与Andrea一起编程对每个人都是免费的。帮助我保持这种方式,看看这个赞助商。

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