[Flutter翻译]为什么要在Flutter中使用隔离物?

889 阅读14分钟

image.png

原文地址:alphamikle.dev/why-should-…

原文作者:hashnode.com/@alphamikle

发布时间:2021年3月16日

image.png

在Flutter中管理状态的方法有很多,但大多数都是以这样的方式构建的,所有的逻辑都是在应用程序的主隔离区中执行的。网络请求的执行,与WebSocket的合作,潜在的重同步操作(如本地搜索),所有这些,作为一个规则,在主隔离区中实现。本文将展示其他门以及👀。

我只看到了一个旨在将这些操作转移到外部隔离区的包,但最近又出现了一个包(由我编写)。我建议你熟悉一下它。

在这篇文章中,我将使用两个主要术语--隔离体和主线程。它们的区别是为了让文字不至于太过同义,但本质上主线也是一种隔离物。另外,在这里你会发现一些表达方式会割伤耳朵(或眼睛)特别敏感的天性,所以我提前道歉--对不起。我会把所有有疑问的词用斜体标出(不光是他们,现在就试着弄明白)。另外,进一步调用操作同步--我会记住,你会在调用第三方方法的同一个函数中收到结果。而异步函数是指你不会在原地得到结果,而是在另一个函数中得到。

介紹

隔离器的设计是为了在您的Flutter应用程序的非主线程上运行代码。当主线程开始执行网络请求,执行计算,或做任何其他操作,除了它的主要目的--绘制界面,你迟早会面临这样一个事实,即渲染一帧的宝贵时间将开始增加。基本上,你在主线程上执行任何操作的时间被限制在16ms,这就是在60FPS下渲染2帧之间存在的窗口。不过,目前有很多手机的显示频率较高,由于我只有一台,所以用不同的方法来比较同样操作的应用的性能,就越有意思。本例中,窗口已经是11.11ms,显示刷新率为90FPS。

实验一:初始条件

让我们想象一下,你需要加载大量的数据,你可以通过几种方式来实现。

  • 只要在主线程上提出一个请求
  • 使用计算函数进行请求
  • 明确使用隔离请求

实验是在一台搭载骁龙855处理器、屏幕频率强制到90Hz的OnePlus 7 Pro上进行的。通过flutter run profile命令启动应用程序。进行了从服务器接收数据的模拟(连续10次同时请求5次)。

一次请求返回JSON--一个2273个元素的数组,其中一个元素如截图所示。答案的大小是1.12Mb。因此,对于5个同步请求,我们需要解析5.6Mb的JSON(但应用列表中会有2273个项目)。

image.png

我们从这样的参数--帧渲染时间、操作时间、组织/编写代码的复杂程度来比较三种方法。

第一个例子。一组来自主线程的请求

Future<void> loadItemsOnMainThread() async {
  _startFpsMeter();
  isLoading = true;
  notifyListeners();
  List<Item> mainThreadItems;
  for (int i = 0; i < 10; i++) {
    bench.startTimer('Load items in main thread');
    mainThreadItems = await makeManyRequests(5);
    final double diff = bench.endTimer('Load items in main thread');
    requestDurations.add(diff);
  }
  items.clear();
  items.addAll(mainThreadItems);
  isLoading = false;
  notifyListeners();
  _stopFpsMeter();
  requestDurations.clear();
}

该方法驻留在应用程序主隔离区执行的反应状态中。

该方法位于反应状态下,在应用程序的主隔离区中执行。当执行上面的代码时,我们得到以下值。

  • 一帧的平均渲染时间(FrameRenderingTime)- 14.036ms / 71.25FPS。
  • 最大 FRT- 100.332ms / 9.97FPS
  • 执行5个并发请求的平均时间 - 226.894ms

请看实际操作。

www.youtube.com/watch?v=oRz…

第二个例子:compute()

Future<void> loadItemsWithComputed() async {
  _startFpsMeter();
  isLoading = true;
  notifyListeners();
  List<Item> computedItems;

  /// There were two variants of execution
  /// Each set of 5 concurrent requests, run sequentially,
  /// ran in compute function
  if (true) {
    for (int i = 0; i < 10; i++) {
      bench.startTimer('Load items in computed');
      computedItems = await compute<dynamic, List<Item>>(_loadItemsWithComputed, null);
      final double diff = bench.endTimer('Load items in computed');
      requestDurations.add(diff);
    }

    /// The second option is all 10 requests of 5 in one compute function
  } else {
    bench.startTimer('Load items in computed');
    computedItems = await compute<dynamic, List<Item>>(_loadAllItemsWithComputed, null);
    final double diff = bench.endTimer('Load items in computed');
    requestDurations.add(diff);
  }
  items.clear();
  items.addAll(computedItems);
  isLoading = false;
  notifyListeners();
  _stopFpsMeter();
  requestDurations.clear();
}

Future<List<Item>> _loadItemsWithComputed([dynamic _]) async {
  return makeManyRequests(5);
}

Future<List<Item>> _loadAllItemsWithComputed([dynamic _]) async {
  List<Item> items;
  for (int i = 0; i < 10; i++) {
    items = await makeManyRequests(5);
  }
  return items;
}

在这个例子中,同样的请求在两个版本中发起:在10个连续请求中,每5个并发请求都在自己的计算中发起。

  • 平均FRT - 11.254ms / 88.86FPS。
  • 最大 FRT - 22.304ms / 44.84FPS
  • 执行5个并发请求的平均时间 - 386.253ms

第二种方式--5个同时请求的10个顺序请求都在一次计算中发起。

  • 平均FRT - 11.252ms / 88.87FPS。
  • 最大 FRT - 22.306ms / 44.83FPS
  • 5个并发请求的平均时间(计算中执行5个请求中的全部10个,除以10)-231.747ms。

www.youtube.com/watch?v=bCI…

第三个例子。隔离

这里值得做一个题外话:在包的术语中,一般状态(state)有两个部分。

  • 前端状态是一个任何反应式的状态,它向后端发送消息,处理它的响应,也存储数据,更新后UI会被更新,它也存储从UI调用的轻量方法。这个状态工作在应用程序的主线程中。
  • Backend-state是一个重状态,它接收来自Frontend的消息,执行重操作,向Frontend返回响应,并在一个单独的隔离区工作。这个状态也可以存储数据(无论你在哪里)。

由于需要与隔离区进行通信,第三种方案的代码被拆分成几个方法。前端方法如下图所示。

/// This method is the entry point to the operation
Future<void> loadItemsWithIsolate() async {
  /// We start the frame counter before the whole operation
  _startFpsMeter();
  isLoading = true;
  notifyListeners();

  /// We start counting the request time
  bench.startTimer('Load items in separate isolate');

  /// Sending an event to Backend
  send(Events.startLoadingItems);
}

/// The [Events.loadingItems] event handler for updating the request time from the isolate
void _middleLoadingEvent() {
  final double time = bench.endTimer('Load items in separate isolate');
  requestDurations.add(time);
  bench.startTimer('Load items in separate isolate');
}

/// The [Events.endLoadingItems] terminating event handler from the isolate
Future<void> _endLoadingEvents(List<Item> items) async {
  this.items.clear();

  /// Updating data in reactive state
  this.items.addAll(items);

  /// Finishing counting request times
  final double time = bench.endTimer('Load items in separate isolate');
  requestDurations.add(time);
  isLoading = false;
  notifyListeners();

  /// Stop the frame counter
  _stopFpsMeter();
  requestDurations.clear();
}

在这里你可以看到后端方法,以及我们需要的逻辑。

/// Event handler [Events.startLoadingItems]
Future<void> _loadingItems() async {
  _items.clear();
  for (int i = 0; i < 10; i++) {
    _items.addAll(await makeManyRequests(5));
    if (i < (10 - 1)) {
      /// For all requests except the last one - we send only one event
      send(Events.loadingItems);
    } else {
      /// For the last of 10 requests - send a message with data
      send(Events.endLoadingItems, _items);
    }
  }
}

结果。

  • 平均FRT - 11.151ms / 89.68FPS
  • 最大 FRT - 11.152ms / 89.67FPS

www.youtube.com/watch?v=Bmk…

小计

在应用中进行三次加载同一数据集的实验后,我们得到以下结果。

image.png

从这些结果来看,可以得出以下结论。

  • Flutter能够稳定地提供90FPS的画面
  • 在您的应用程序的主线程上进行大量的网络请求会影响其性能--出现延迟。
  • 编写在主线程上运行的代码,就像剥梨子一样容易。
  • 计算允许你减少滞后的可见性。
  • 使用Compute写代码有一定的局限性(纯函数,静态方法不能传递,不能用闭包等)。
  • 按操作时间计算,使用compute时的开销~150-160ms。
  • 隔离完全消除滞后
  • 使用隔离体编写代码是比较困难的,而且也有一些局限性,这一点将在后面讨论。

让我们再进行一次实验,以确定哪种方法对所研究的所有参数都是最优的。

实验二:局部搜索

让我们想象一下,现在我们需要通过输入的值在加载的数据中找到某些元素。这个测试是这样实现的:有一个输入,从项目列表中可用的子串数中逐字输入3个字符的子串。用于搜索的数组中的元素数量增加了10倍,为22730个。

搜索以2种模式进行--从列表中的元素中输入字符串的原始存在,以及使用字符串相似性算法

另外,异步搜索选项--compute / Isolate,在前一个搜索完成之前不会启动。它的工作原理是这样的--在输入字段中输入第一个字符,我们就开始搜索,直到搜索完成--数据不会返回主线程,UI也不会重新绘制,第二个字符不会在输入字段中输入。当所有操作完成后,输入第二个字符,也反之。这类似于我们 "保存 "用户输入的字符时的算法,然后只发送一个请求,而不是为绝对每一个输入的字符发送一个请求,不管它们输入得多快。

渲染时间只在字符输入搜索时进行测量,即数据准备操作,其他任何事情都不会影响收集到的数据。

对于初学者来说,帮助函数,搜索函数和其他通用代码。

/// Function for creating a copy of elements
/// used as source for filtering
void cacheItems() {
  _notFilteredItems.clear();
  final List<Item> multipliedItems = [];
  for (int i = 0; i < 10; i++) {
    multipliedItems.addAll(items);
  }
  _notFilteredItems.addAll(multipliedItems);
}
/// Function that launches a test script
/// for entering characters into a text input
Future<void> _testSearch() async {
  List<String> words = items.map((Item item) => item.profile.replaceAll('https://opencollective.com/', '')).toSet().toList();
  words = words
      .map((String word) {
        final String newWord = word.substring(0, min(word.length, 3));
        return newWord;
      })
      .toSet()
      .take(3)
      .toList();

  /// Start the frame counter
  _startFpsMeter();
  for (String word in words) {
    final List<String> letters = word.split('');
    String search = '';
    for (String letter in letters) {
      search += letter;
      await _setWord(search);
    }
    while (search.isNotEmpty) {
      search = search.substring(0, search.length - 1);
      await _setWord(search);
    }
  }

  /// Stop the frame counter
  _stopFpsMeter();
}
/// We enter characters with a delay of 800ms,
/// but if the data from the asynchronous filter (computed / isolate)
/// has not yet arrived, then we are waiting for them
Future<void> _setWord(String word) async {
  if (!canPlaceNextLetter) {
    await wait(800);
    await _setWord(word);
  } else {
    searchController.value = TextEditingValue(text: word);
    await wait(800);
  }
}
/// Depending on the set flag [USE_SIMILARITY]
/// whether or not search with string similarity is used
List<Item> filterItems(Packet2<List<Item>, String> itemsAndInputValue) {
  return itemsAndInputValue.value.where((Item item) {
    return item.profile.contains(itemsAndInputValue.value2) || (USE_SIMILARITY && isStringsSimilar(item.profile, itemsAndInputValue.value2));
  }).toList();
}

bool isStringsSimilar(String first, String second) {
  return max(StringSimilarity.compareTwoStrings(first, second), StringSimilarity.compareTwoStrings(second, first)) >= 0.3);
}

在主线中搜索

Future<void> runSearchOnMainThread() async {
  cacheItems();
  isLoading = true;
  notifyListeners();
  searchController.addListener(_searchOnMainThread);
  await _testSearch();
  searchController.removeListener(_searchOnMainThread);
  isLoading = false;
  notifyListeners();
}

void _searchOnMainThread() {
  final String searchValue = searchController.text;
  if (searchValue.isEmpty && items.length != _notFilteredItems.length) {
    items.clear();
    items.addAll(_notFilteredItems);
    notifyListeners();
    return;
  }
  items.clear();

  /// Packet2 - wrapper class for two values
  items.addAll(filterItems(Packet2(_notFilteredItems, searchValue)));
  notifyListeners();
}

这就是它的样子。

www.youtube.com/watch?v=Vlc…

www.youtube.com/watch?v=XpT…

简单的搜索结果。

  • 平均FRT - 21.588ms / 46.32FPS。
  • 最大 FRT - 668,986ms / 1.50FPS

用相似性结果搜索。

  • 平均FRT - 43,123ms / 23.19FPS。
  • 最大 FRT - 2 440,910ms / 0.41FPS

用compute()搜索

Future<void> runSearchWithCompute() async {
  cacheItems();
  isLoading = true;
  notifyListeners();
  searchController.addListener(_searchWithCompute);
  await _testSearch();
  searchController.removeListener(_searchWithCompute);
  isLoading = false;
  notifyListeners();
}

Future<void> _searchWithCompute() async {
  canPlaceNextLetter = false;

  /// Before starting filtering, set a flag that will signal
  /// that asynchronous filtering is taking place
  isSearching = true;
  notifyListeners();
  final String searchValue = searchController.text;
  if (searchValue.isEmpty && items.length != _notFilteredItems.length) {
    items.clear();
    items.addAll(_notFilteredItems);
    isSearching = false;
    notifyListeners();
    await wait(800);
    canPlaceNextLetter = true;
    return;
  }
  final List<Item> filteredItems = await compute(filterItems, Packet2(_notFilteredItems, searchValue));

  /// After filtering is finished, remove the signal
  isSearching = false;
  notifyListeners();
  await wait(800);
  items.clear();
  items.addAll(filteredItems);
  notifyListeners();
  canPlaceNextLetter = true;
}

一些YouTube。

www.youtube.com/watch?v=ecv…

www.youtube.com/watch?v=yv2…

简单的搜索结果。

  • 平均FRT - 12.682ms / 78.85FPS。
  • 最大 FRT - 111.544ms / 8.97FPS

用相似性结果搜索。

  • 平均FRT - 12.515ms / 79.90FPS。
  • 最大 FRT - 111,527ms / 8.97FPS

用Isolate搜索

代码不多。前端

/// Start operation in isolate by sending message
Future<void> runSearchInIsolate() async {
  send(Events.cacheItems);
}

void _middleLoadingEvent() {
  final double time = bench.endTimer('Load items in separate isolate');
  requestDurations.add(time);
  bench.startTimer('Load items in separate isolate');
}

/// This method will called on event [Events.cacheItems], which will sent by Backend
Future<void> _startSearchOnIsolate() async {
  isLoading = true;
  notifyListeners();
  searchController.addListener(_searchInIsolate);
  await _testSearch();
  searchController.removeListener(_searchInIsolate);
  isLoading = false;
  notifyListeners();
}

/// On every input event we send message to Backend
void _searchInIsolate() {
  canPlaceNextLetter = false;
  isSearching = true;
  notifyListeners();
  send(Events.startSearch, searchController.text);
}

/// Writing data from Backend (isolate) to Frontend (reactive state)
Future<void> _setFilteredItems(List<Item> filteredItems) async {
  isSearching = false;
  notifyListeners();
  await wait(800);
  items.clear();
  items.addAll(filteredItems);
  notifyListeners();
  canPlaceNextLetter = true;
}

Future<void> _endLoadingEvents(List<Item> items) async {
  this.items.clear();
  this.items.addAll(items);
  final double time = bench.endTimer('Load items in separate isolate');
  requestDurations.add(time);
  await wait(800);
  isLoading = false;
  notifyListeners();
  _stopFpsMeter();
  print('Load items in isolate ->' + requestDurations.join(' ').replaceAll('.', ','));
  requestDurations.clear();
}

而这些方法都是在第三方隔离区运行的后台。

/// Handler for event [Events.cacheItems]
void _cacheItems() {
  _notFilteredItems.clear();
  final List<Item> multipliedItems = [];
  for (int i = 0; i < 10; i++) {
    multipliedItems.addAll(_items);
  }
  _notFilteredItems.addAll(multipliedItems);
  send(Events.cacheItems);
}

/// For each event [Events.startSearch] this method is called,
/// filtering elements and sending the filtered to the light state
void _filterItems(String searchValue) {
  if (searchValue.isEmpty) {
    _items.clear();
    _items.addAll(_notFilteredItems);
    send(ThirdEvents.setFilteredItems, _items);
    return;
  }
  final List<Item> filteredItems = filterItems(Packet2(_notFilteredItems, searchValue));
  _items.clear();
  _items.addAll(filteredItems);
  send(Events.setFilteredItems, _items);
}

简单的搜索结果。

  • 平均FRT - 11.354ms / 88.08FPS
  • 最大 FRT - 33.455ms / 29.89FPS

用相似性搜索。

  • 平均FRT - 11.353ms / 88.08FPS。
  • 最大 FRT - 33.459ms / 29.89FPS

更多结论

image.png

从这个平板电脑和之前的研究来看。

  • 主线不应该用于大于16ms的操作(至少提供60FPS)。
  • Compute在技术上适用于频繁和大量的操作,但会带来同样150ms的开销,而且与永久隔离相比,性能也更不稳定(这可能是由于每次打开,和操作完成后,隔离都会被关闭 ,这也需要资源)。
  • 在Flutter应用中,隔离是实现最大性能的最难的代码方式。

隔离器

好吧,看来隔离是实现这个结果的理想方式,连Google都建议所有的重度操作都使用隔离(这是为了口口相传,我没有找到任何证明)。但是你必须要写很多代码。事实上,上面所写的一切都是使用最开始介绍的库实现的结果,如果没有它--你将不得不写很多很多。另外,这个搜索算法还可以进行优化--在过滤完所有元素后,只发送一小部分数据到前面--这样会占用更少的资源,在其传输后,再发送其他所有的数据。或者按块发送数据。

我还对隔离体之间的通信通道的带宽进行了实验。为了评估它,使用了以下实体。

class Item {
  const Item(
    this.id,
    this.createdAt,
    this.profile,
    this.imageUrl,
  );

  final int id;
  final DateTime createdAt;
  final String profile;
  final String imageUrl;
}

而结果是--同时传输5000个元素,复制数据所需的时间并不影响UI,即渲染频率并没有降低。1,000,000个这样的元素,通过Future.delayed分批传输,每次5000个,在8ms的突发传输之间强制暂停,而帧率并没有下降到80FPS以下。遗憾的是,我在写这篇文章之前很久就做了这个实验,没有干货数据(如果有要求,会出现)。

很多人可能觉得处理隔离器很困难或者没有必要,大家就止步于计算。在这里,这个包的另一个功能可以派上用场,它把API等同于计算的简单性,结果,它给了更多的可能性。

下面是一个例子。

/// Frontend part
Future<void> decrement([int diff = 1]) async {
  counter = await runBackendMethod<int, int>(Events.decrement, diff);
}

/// -----

/// Backend part
Future<int> _decrement(int diff) async {
  counter -= diff;
  return counter;
}

由于这种方法,你可以简单地通过这个函数对应的ID来调用后台函数。ID与方法的匹配是在预定义的getter中指定的。

/// Frontend part
/// This block is responsible for handling events from the isolate.
@override
Map<Events, Function> get tasks => {
      Events.increment: _setCounter,
      Events.decrement: _setCounter,
      Events.error: _setCounter,
    };

/// -----

/// Backend part
/// And this one is for handling events from the main thread
@override
Map<Events, Function> get operations => {
      Events.increment: _increment,
      Events.decrement: _decrement,
    };

因此,我们得到两种交互方式。

  1. 通过明确的消息传递进行异步通信

前端使用send方法向后端发送一个事件,在消息中传递事件ID和一个可选的参数。

enum Events {
  increment,
}

class FirstState with Frontend<Events> {
  int counter = 0;

  void increment([int diff = 1]) {
    send(Events.increment, diff);
  }

  void _setCounter(int value) {
    counter = value;
    notifyListeners();
  }

  @override
  Map<Events, Function> get tasks => {
    Events.increment: _setCounter,
  };
}

该消息被传递到后台并在那里进行处理。

class FirstBackend extends Backend<Events, void> {
  FirstBackend(BackendArgument<void> argument) : super(argument);

  int counter = 0;

  void _increment(int diff) {
    counter += diff;
    send(Events.increment, counter);
  }

  @override
  Map<Events, Function> get operations => {
    Events.increment: _increment,
  };
}

后台将结果返回给前台就可以了! 返回结果有两种方式--通过使用后台方法(return)返回响应(那么响应将以与接收到的消息ID相同的消息发送),第二种是显式调用发送方法。在这种情况下,你可以用你指定的任何ID向反应状态发送任何消息。最主要的是,处理方法是由这些ID设置的。

从原理上看,第一种方式是这样的。

image.png

黄色双面箭头--与外界的任何服务进行交互,例如,某个服务器。而紫色的,从服务器到后端--这些是来自同一服务器的传入消息,例如--WebSocket。

  1. 通过调用后端函数的ID来实现同步通信

前端使用runBackendMethod方法,指定一个ID来调用对应的后端方法,直接得到响应。这样一来,甚至不需要在你的front的任务列表中指定什么。同时,如下面的代码所示,你可以在你的前端覆盖onBackendResponse方法,每次你的前端状态收到来自后端的消息时,都会调用这个方法。

enum Events {
  decrement,
}

class FirstState with ChangeNotifier, Frontend<Events> {
  int counter = 0;

  Future<void> decrement([int diff = 1]) async {
    counter = await runBackendMethod<int, int>(Events.decrement, diff);
  }

  /// Automatically notification after any event from backend
  @override
  void onBackendResponse() {
    notifyListeners();
  }
}

后面的方法对传入的事件进行处理,并简单地返回结果。在这种情况下,有一个限制--被称为 "同步 "的后端方法不应该调用它们所对应的相同ID的发送方法。在这个例子中,_decrement方法不应该调用发送(Events.decrement)方法。同时,他可以发送任何其他消息。

class FirstBackend extends Backend<Events, void> {
  FirstBackend(BackendArgument<void> argument) : super(argument);

  int counter = 0;

  /// Or, you can simply return a value
  Future<int> _decrement(int diff) async {
    counter -= diff;
    return counter;
  }

  @override
  Map<Events, Function> get operations => {
    Events.decrement: _decrement,
  };
}

第二种方式的方案与第一种类似,只是在前面你不需要写来自后面的事件处理程序。

image.png

很快,在0.0.5版本中,这个功能就可以工作了,并且在后退中--你可以从它的后台以同步模式运行Frontend的任务。

还有什么要添加的...

要使用这样的捆绑,你需要创建这些后端。为此,Frontend<EventType>有一个后端创建机制--initBackend方法。在这个方法中,你需要传递一个工厂函数来创建后端。它应该是一个纯顶层函数(Flutter文档中说的顶层),或者是一个静态类方法。创建一个隔离体的时间大约是200ms。

enum Events {
  increment,
  decrement,
}

class FirstState with ChangeNotifier, Frontend<Events> {
  int counter = 0;

  void increment([int diff = 1]) {
    send(Events.increment, diff);
  }

  Future<void> decrement([int diff = 1]) async {
    counter = await runBackendMethod<int, int>(Events.decrement, diff);
  }

  void _setCounter(int value) {
    counter = value;
  }

  Future<void> initState() async {
    await initBackend(createFirstBackend);
  }

  /// Automatically notification after any event from backend
  @override
  void onBackendResponse() {
    notifyListeners();
  }

  @override
  Map<Events, Function> get tasks => {
    Events.increment: _setCounter,
  };
}

后台部件创建函数的一个例子。

typedef Creator<TDataType> = void Function(BackendArgument<TDataType> argument);

void createFirstBackend(BackendArgument<void> argument) {
  FirstBackend(argument.toFrontend);
}

@protected
Future<void> initBackend<TDataType extends Object>(Creator<TDataType> creator, {TDataType data, ErrorHandler errorHandler}) async {
  /// ...
}

局限性

  • 一切都和普通隔离剂一样
  • 对于每一个正在创建的 "后端",当前都在创建自己的隔离区,如果后端太多,它们的创建时间就会变得很明显,特别是当你初始化所有的后端时,比如说在加载应用程序的时候。我实验过同时运行30个后端--上述手机在--释放模式下的启动时间需要6秒多。
  • 在处理隔离体(后端)出现的错误时,存在一些困难。在这里,如果你对这个包感兴趣,你应该更详细地熟悉Frontend的initBackend方法。
  • 与只在主线程中存储逻辑相比,编写代码的复杂度较高

使用清单

这里的一切都很简单,你不需要使用隔离剂(无论是单独使用还是使用这个包),如果。

  • 你的应用程序在各种操作下的性能都不会下降。
  • 对于瓶颈问题,计算就够了。
  • 你不会想处理隔离物吧?
  • 你的应用程序的生命周期很短,所以没有必要优化它。

否则,你可以将注意力转向这种方法和一个(称为Isolator),它将简化你对隔离物的工作。

本文的所有例子都可以在Github上找到。


通过www.DeepL.com/Translator(免费版)翻译