Skip to content

Commit ec22d90

Browse files
authored
iOS: Improve thread safety of first frame callback (flutter/engine#55966)
Previously, we dispatched a task to a background thread to wait for the first frame to be ready. In the background task, we then throw a task on the main thread that invokes that user-provided callback to notify them that the first frame is ready. During ARC migration, we ensured that self was strongly-captured in the task that runs on the main thread, in order to prevent the possibility that the last remaining reference to the FlutterEngine instance be the one on the background thread. However, with the previous code, it's possible that the callback is dispatched to the main thread, executes, and completes before the block on the background thread finishes. In that case, the last remaining FlutterEngine reference may *still* be the one on the background thread. In order to prevent this scenario, we use `dispatch_group_notify` to ensure that the tasks are executed sequentially, and that the callback task is not dispatched to the main thread until the background task has completed. Further, we capture FlutterEngine strongly in the second task to ensure that *it* is the last remaining task. Why do we need to capture self strongly in the task fired to the main queue? Imagine the following: * We queue up the first task on the background thread. * We queue up the callback task on the main thread to be executed only after the background task completes. * The task on the background thread starts running. * That task captures weakSelf strongly and checks it -- it's not nil, yay. * That task now makes the blocking first frame call. Time passes. While that time passes all other strong references to self go away. That second block only captured self weakly, so the background task now has the only remaining strong reference. Uh oh! * We hit the end of the background task block and since we have the last remaining strong reference, we dealloc FlutterEngine... on the background thread. * KABOOM. No changes to tests since this is a fix to a race condition that doesn't affect app semantics, isn't testable as written, and adding shims to improve testability will likely make some already subtle/difficult-to-reason-about code even more complex and difficult to reason about. Issue: flutter#156177 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 3031222 commit ec22d90

File tree

1 file changed

+23
-20
lines changed

1 file changed

+23
-20
lines changed

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,34 +1359,37 @@ - (void)onLocaleUpdated:(NSNotification*)notification {
13591359
- (void)waitForFirstFrame:(NSTimeInterval)timeout
13601360
callback:(void (^_Nonnull)(BOOL didTimeout))callback {
13611361
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0);
1362+
dispatch_group_t group = dispatch_group_create();
1363+
13621364
__weak FlutterEngine* weakSelf = self;
1363-
dispatch_async(queue, ^{
1365+
__block BOOL didTimeout = NO;
1366+
dispatch_group_async(group, queue, ^{
13641367
FlutterEngine* strongSelf = weakSelf;
13651368
if (!strongSelf) {
13661369
return;
13671370
}
13681371

13691372
fml::TimeDelta waitTime = fml::TimeDelta::FromMilliseconds(timeout * 1000);
1370-
BOOL didTimeout =
1373+
didTimeout =
13711374
strongSelf.shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded;
1372-
dispatch_async(dispatch_get_main_queue(), ^{
1373-
// Capture strongSelf to ensure that destruction does not occur on a background thread.
1374-
//
1375-
// The containing block, executed on a background thread, strongly captures self, then makes a
1376-
// blocking call to self.shell.WaitForFirstFrame(). If, during this time, all other instances
1377-
// of self are released, the containing block's reference would be the last one, resulting in
1378-
// `[FlutterEngine dealloc]` being called when it goes out of scope at the end of that block,
1379-
// on a background thread. FlutterEngine owns a reference to a PlatformViewsController, which
1380-
// owns a WeakPtrFactory whose destructor asserts that it be freed on the platform thread. To
1381-
// avoid this, we capture strongSelf in the current block, which is executed on the platform
1382-
// thread.
1383-
//
1384-
// strongSelf is never nil here since it's a strong reference that's verified non-nil above,
1385-
// but we use a conditional check to avoid and unused expression compiler warning.
1386-
if (strongSelf) {
1387-
callback(didTimeout);
1388-
}
1389-
});
1375+
});
1376+
1377+
// Only execute the main queue task once the background task has completely finished executing.
1378+
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
1379+
// Strongly capture self on the task dispatched to the main thread.
1380+
//
1381+
// When we capture weakSelf strongly in the above block on a background thread, we risk the
1382+
// possibility that all other strong references to FlutterEngine go out of scope while the block
1383+
// executes and that the engine is dealloc'ed at the end of the above block on a background
1384+
// thread. FlutterEngine is not safe to release on any thread other than the main thread.
1385+
//
1386+
// self is never nil here since it's a strong reference that's verified non-nil above, but we
1387+
// use a conditional check to avoid an unused expression compiler warning.
1388+
FlutterEngine* strongSelf = self;
1389+
if (!strongSelf) {
1390+
return;
1391+
}
1392+
callback(didTimeout);
13901393
});
13911394
}
13921395

0 commit comments

Comments
 (0)