Dart 中的事件循环和微任务(scheduleMicrotask)

792 阅读10分钟

Dart 应用程序有一个带有两个队列的事件循环 —— 事件 队列微任务队列

Dart 程序先创建主isolate

Dart 程序的执行流程始于主 isolatemain 函数,然后可以通过隔离体机制创建和管理其他隔离体。每个子隔离体都可以有自己的入口点函数, 也就是耗时任务的函数。

事件循环和队列

在客户端应用中,主 isolate 的事件队列内,可能会包含重绘的请求、点击的通知或者(计时器、Dart 隔离之间的消息)其他界面事件。例如,下图展示了包含四个事件的事件队列,队列会按照先进先出的模式处理事件。

event-loop.png

如下图所示,在 main() 方法执行完毕后,事件队列中的处理才开始,此时处理的是第一个重绘的事件。而后主 isolate 会处理点击事件,接着再处理另一个重绘事件。

event-handling.png

下面这张图, 基本上可以表示事件模型。

both-queues.png

当事件循环执行微任务队列中的任务时,事件队列会被卡住:应用程序无法绘制图形、处理鼠标单击、对 I/O 做出反应等。如下图所示。

event-jank.png

在一个客户端应用中,耗时过长的同步操作,通常会导致 卡顿的动画。而最糟糕的是,应用界面可能完全失去响应。 这种情况, 我们可以考虑上一篇张讲的开启一个子isolate, 来处理耗时过长的同步任务。

main函数的回调触发流程

enginec++部分

engine运行dart层代码是通过UITaskRunner在UI Thread中执行的,这就是前面说的创建UI Thread的主要作用,下面看下engine的Run方法。

/flutter/shell/common/engine.cc

Engine::RunStatus Engine::Run(RunConfiguration configuration) {
  ...

  auto isolate_launch_status =
      PrepareAndLaunchIsolate(std::move(configuration));
      
  ...

  return isolate_running ? Engine::RunStatus::Success
                         : Engine::RunStatus::Failure;
}

shell::Engine::RunStatus Engine::PrepareAndLaunchIsolate(
    RunConfiguration configuration) {
  ...

  auto isolate_configuration = configuration.TakeIsolateConfiguration();

  std::shared_ptr<blink::DartIsolate> isolate =
      runtime_controller_->GetRootIsolate().lock();

  ...

  if (configuration.GetEntrypointLibrary().empty()) {
    if (!isolate->Run(configuration.GetEntrypoint())) {
      ...
    }
  } else {
    ...
  }

  return RunStatus::Success;
}

最终会通过DartIsolate的Run方法来执行

/flutter/runtime/dart_isolate.cc

bool DartIsolate::Run(const std::string& entrypoint_name) {
  ...

  Dart_Handle entrypoint =
      Dart_GetField(Dart_RootLibrary(), tonic::ToDart(entrypoint_name.c_str()));
  ...

  Dart_Handle isolate_lib = Dart_LookupLibrary(tonic::ToDart("dart:isolate"));
  if (tonic::LogIfError(isolate_lib)) {
    return false;
  }

  Dart_Handle isolate_args[] = {
      entrypoint,
      Dart_Null(),
  };

  if (tonic::LogIfError(Dart_Invoke(
          isolate_lib, tonic::ToDart("_startMainIsolate"),
          sizeof(isolate_args) / sizeof(isolate_args[0]), isolate_args))) {
    return false;
  }

  ...
  return true;
}

我们可以看到, engine层, 先执行了ToDart("_startMainIsolate")函数。这个函数在dart层的实现在/third_party/dart/runtime/lib/isolate_patch.dart文件里面。下面断点查看,就能发现。

框架层面,main函数的触发是由_RawReceivePortImpl#_handleMessage 方法触发的:

main_start.png

_startMainIsolate

/**
 * Takes the real entry point as argument and schedules it to run in the message
 * queue.
 */
@pragma("vm:entry-point", "call")
void _startMainIsolate(Function entryPoint, List<String>? args) {
  _delayEntrypointInvocation(entryPoint, args, null, true);
}

程序的实际入口点函数 _startMainIsolate 标记为 Dart VM 的入口点,并且通过调用 _delayEntrypointInvocation 函数来延迟执行入口点函数。这是 Dart VM 启动和执行 Dart 程序的一部分。

_delayEntrypointInvocation

void _delayEntrypointInvocation(Function entryPoint, List<String>? args,
    Object? message, bool allowZeroOneOrTwoArgs) {
  final port = RawReceivePort();
  port.handler = (_) { /// mark1 触发回调
    port.close();
    if (allowZeroOneOrTwoArgs) {
      if (entryPoint is _BinaryFunction) {
        (entryPoint as Function)(args, message);
      } else if (entryPoint is _UnaryFunction) {
        (entryPoint as Function)(args);
      } else { /// mark2 触发回调(entryPoint就是整个app的main函数)
        entryPoint();
      }
    } else {
      entryPoint(message);
    }
  };
  port.sendPort.send(null);
}

处理入口函数的延迟调用的。

  1. 创建一个 RawReceivePort 对象,这是 Dart 中用于在隔离体之间进行消息通信的一种机制。

  2. 为这个 RawReceivePort 对象设置一个事件处理器(handler),当接收到消息时,这个处理器会被触发。

  3. 在事件处理器中,首先关闭了这个 RawReceivePort,以确保它只能被触发一次。

main 函数的触发也涉及 消息通知机制_startIsolate 触发 _delayEntrypointInvocation 方法,其中创建 RawReceivePort 接收端看对象,并为 handler 赋值。

_startIsolate

void _startIsolate(
    Function entryPoint, List<String>? args, Object? message, bool isSpawnUri) {
  _delayEntrypointInvocation(entryPoint, args, message, isSpawnUri);
}

触发main方法回调

也就是说收到消息,触发 _RawReceivePortImpl#_handleMessage 时,执行的 handler 就是 mark1 所示的函数。 mark2 的 entryPoint() 方法就是 main 方法。

截屏2023-09-21 11.56.36.png

_runPendingImmediateCallback就是触发的微任务回调。

scheduleMicrotask函数的回调触发流程

scheduleMicrotask(() {
  debugPrint("微任务");
});

scheduleMicrotask将一个任务标记为微任务, 并添加到微任务队列中。

scheduleMicrotask

@pragma('vm:entry-point', 'call')
void scheduleMicrotask(void Function() callback) {
  _Zone currentZone = Zone._current;
  if (identical(_rootZone, currentZone)) {
    // No need to bind the callback. We know that the root's scheduleMicrotask
    // will be invoked in the root zone.
    _rootScheduleMicrotask(null, null, _rootZone, callback);
    return;
  }
  _ZoneFunction implementation = currentZone._scheduleMicrotask;
  if (identical(_rootZone, implementation.zone) &&
      _rootZone.inSameErrorZone(currentZone)) {
    _rootScheduleMicrotask(
        null, null, currentZone, currentZone.registerCallback(callback));
    return;
  }
  Zone.current.scheduleMicrotask(Zone.current.bindCallbackGuarded(callback));
}

调度微任务(microtask)的实现部分。

  1. void scheduleMicrotask(void Function() callback) 是一个公共函数,用于安排微任务的执行。它接受一个回调函数 callback 作为参数,这个回调函数将在微任务队列中执行。
  2. 首先,代码检查当前所处的区域(_Zone)是否为根区域(_rootZone)。根区域通常是 Dart 程序的最外层区域,不需要额外的绑定。如果当前区域是根区域,它会直接调用根区域的 _rootScheduleMicrotask 函数来安排微任务的执行。
  3. 如果当前区域不是根区域,代码会检查当前区域的微任务调度实现,即 currentZone._scheduleMicrotask。如果当前区域的调度器(scheduler)是根区域的调度器,并且当前区域与根区域位于同一错误区域(error zone),则直接使用根区域的 _rootScheduleMicrotask 来安排微任务的执行。
  4. 如果上述两个条件都不满足,说明当前区域具有自定义的微任务调度器,它会使用 Zone.current.scheduleMicrotask(Zone.current.bindCallbackGuarded(callback)) 来调度微任务的执行。这会将回调函数绑定到当前区域,以确保它在适当的上下文中执行。

_rootScheduleMicrotask

void _rootScheduleMicrotask(
    Zone? self, ZoneDelegate? parent, Zone zone, void f()) {
  if (!identical(_rootZone, zone)) {
    bool hasErrorHandler = !_rootZone.inSameErrorZone(zone);
    if (hasErrorHandler) {
      f = zone.bindCallbackGuarded(f);
    } else {
      f = zone.bindCallback(f);
    }
  }
  _scheduleAsyncCallback(f);
}

在根区域下安排微任务的执行。继续执行_scheduleAsyncCallback

_scheduleAsyncCallback

/// Schedules a callback to be called as a microtask.
///
/// The microtask is called after all other currently scheduled
/// microtasks, but as part of the current system event.
void _scheduleAsyncCallback(_AsyncCallback callback) {
  _AsyncCallbackEntry newEntry = new _AsyncCallbackEntry(callback);
  _AsyncCallbackEntry? lastCallback = _lastCallback;
  if (lastCallback == null) {
    _nextCallback = _lastCallback = newEntry;
    if (!_isInCallbackLoop) {
      _AsyncRun._scheduleImmediate(_startMicrotaskLoop);
    }
  } else {
    lastCallback.next = newEntry;
    _lastCallback = newEntry;
  }
}
  1. void _scheduleAsyncCallback(_AsyncCallback callback) 是一个函数,用于将微任务回调函数 callback 进行调度。
  2. 创建一个新的 _AsyncCallbackEntry,将回调函数 callback 包装在其中,以便稍后执行。
  3. 检查 _lastCallback,它是上一个微任务的入口(Entry)。如果当前没有已计划的微任务,则 _lastCallbacknull
  4. 如果没有已计划的微任务(即 _lastCallbacknull),则将新的微任务入口 _AsyncCallbackEntry 赋值给 _nextCallback_lastCallback。这表示当前计划的微任务是队列中的唯一微任务。如果不在回调循环中(_isInCallbackLoopfalse),则调用 _AsyncRun._scheduleImmediate(_startMicrotaskLoop) 来启动微任务循环。
  5. 如果已经有其他微任务计划了(即 _lastCallback 不为 null),则将新的微任务入口 _AsyncCallbackEntry 追加到已计划微任务链表的末尾,确保微任务按顺序执行。

双向链表。

将微任务回调函数 callback 添加到微任务队列中,以在当前事件循环结束时执行。微任务通常用于执行异步操作的回调,确保它们在其他事件完成后立即执行。

_isInCallbackLoop

/// Whether we are currently inside the callback loop.
///
/// If we are inside the loop, we never need to schedule the loop,
/// even if adding a first element.
bool _isInCallbackLoop = false;

这个变量的作用是确保在微任务回调函数执行期间不重复调度微任务回调循环。如果 _isInCallbackLooptrue,则表示当前已经在微任务回调循环中,无需再次调度它。这可以提高性能并避免不必要的重复回调。

_runPendingImmediateCallback

@pragma("vm:entry-point", "call")
void _runPendingImmediateCallback() {
  final callback = _pendingImmediateCallback;
  if (callback != null) {
    _pendingImmediateCallback = null;
    callback();
  }
}

_runPendingImmediateCallback会去执行回调_pendingImmediateCallback。从下面的函数流程看, _pendingImmediateCallback执行的是_startMicrotaskLoop函数。

截屏2023-09-21 16.54.19.png

_startMicrotaskLoop

void _startMicrotaskLoop() {
  _isInCallbackLoop = true;
  try {
    // Moved to separate function because try-finally prevents
    // good optimization.
    _microtaskLoop();
  } finally {
    _lastPriorityCallback = null;
    _isInCallbackLoop = false;
    if (_nextCallback != null) {
      _AsyncRun._scheduleImmediate(_startMicrotaskLoop);
    }
  }
}
  1. _startMicrotaskLoop 函数:这是启动微任务回调循环的函数。当被调用时,它会执行微任务回调循环(在 _microtaskLoop 函数中执行)。这个函数的目的是确保微任务回调循环只被调度一次。
  2. _isInCallbackLoop 变量:这个布尔值用于标识是否当前正在微任务回调循环中。如果 _isInCallbackLooptrue,则表示已经在微任务回调循环中,不需要再次调度。
  3. try-finally 块:这是一个异常处理机制。在 try 块中, _microtaskLoop 函数被调用以执行微任务回调。try-finally 块确保在 _microtaskLoop 函数执行后(不论是否发生异常),以下的代码都会被执行。
  4. _lastPriorityCallback 的清除:在 finally 块中, _lastPriorityCallback 变量被设置为 null。这个变量通常用于管理 Dart 中的任务调度优先级,清除它表示微任务回调循环已经结束。
  5. _isInCallbackLoop 的重置:在 finally 块中,_isInCallbackLoop 变量被设置为 false,表示微任务回调循环已经结束。
  6. _nextCallback 的检查和重新调度:最后,代码检查是否还有待处理的微任务回调(存在于 _nextCallback 中)。如果有,则通过 _AsyncRun._scheduleImmediate(_startMicrotaskLoop) 重新调度微任务回调循环,以便继续处理微任务。

确保微任务回调循环在需要时被正确调度,并在执行完成后能够根据需要重新调度。

_microtaskLoop移动到单独函数, 因为try-finally预防。 这是个很好的优化。

_microtaskLoop

void _microtaskLoop() {
  for (var entry = _nextCallback; entry != null; entry = _nextCallback) {
    _lastPriorityCallback = null;
    var next = entry.next;
    _nextCallback = next;
    if (next == null) _lastCallback = null;
    (entry.callback)();
  }
}

它是 Dart 中微任务回调循环的核心部分。

  1. _microtaskLoop 函数:这是微任务回调循环的主要函数,用于处理微任务队列中的回调函数。
  2. 循环:for 循环用于遍历微任务队列,每次迭代处理一个微任务。
  3. _nextCallback_nextCallback 是一个指向下一个微任务回调的指针。在每次迭代中,从 _nextCallback 中获取下一个微任务回调。
  4. _lastPriorityCallback 的清除:在每次迭代开始时,将 _lastPriorityCallback 设置为 null,这表示在当前迭代中没有具有特殊优先级的回调。
  5. var next = entry.next;:这行代码用于获取当前迭代中微任务队列中的下一个回调。
  6. _nextCallback 的更新:_nextCallback 被更新为 next,以便在下一次迭代中处理下一个微任务回调。
  7. _lastCallback 的清除:如果 nextnull,则表示微任务队列中已经没有待处理的回调,因此将 _lastCallback 设置为 null
  8. 执行回调:(entry.callback)(); 这行代码执行当前微任务回调的函数体,即调用回调函数。

由上面的核心代码可知, 从微任务队列中逐个获取并执行微任务回调函数。每次迭代处理一个微任务,确保微任务按照它们加入队列的顺序执行。微任务回调通常用于处理 Dart 中的异步操作,例如 FutureStream 中的回调函数,以及通过 scheduleMicrotask 调度的任务。

一个永不会执行的示例

官方源码提供了一个永不会执行的示例。

/// main() {
///   Timer.run(() { print("executed"); });  // Will never be executed.
///   foo() {
///     scheduleMicrotask(foo);  // Schedules [foo] in front of other events.
///   }
///   foo();
/// }

据官方解释。

  1. scheduleMicrotask 调度微任务的函数。微任务是异步任务,通常用于执行轻量级的工作,如回调函数、状态更新等。
  2. 异步执行:scheduleMicrotask 注册的回调函数会异步执行,这些回调函数会按照它们被注册的顺序依次执行,并且会在其他异步事件(如 Timer 事件或 DOM 事件)之前执行。
  3. 潜在的警告:代码注释中提到了一个潜在的警告,即通过 scheduleMicrotask 注册的回调函数可能会占用太多的执行时间,从而导致其他异步事件(如 Timer 回调)无法及时执行。这种情况下,可能会导致应用程序变得不响应,因此需要谨慎使用 scheduleMicrotask
  4. 示例:代码注释中提供了一个示例,展示了如何通过 scheduleMicrotask 注册回调函数,并且强调了这些回调函数可能在其他异步事件之前执行。在示例中,foo 函数通过 scheduleMicrotask 调度自身,导致它被不断执行,而 Timer.run 中的回调函数永远不会执行。

参考资料

Dart 中的并发

Dart asynchronous programming: Isolates and event loops

探索 Dart 消息处理与微任务循环

读代码-DartVM启动加载

Flutter启动流程源码分析

2.8 Flutter异常捕获