From 0d19d46b441050877bd35ee453b5b6d5645cb0dd Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Fri, 9 Sep 2022 15:14:21 -0700 Subject: [PATCH] Started handling messages from background isolates. (#109005) Started handling messages from background isolates. --- .../android/app/src/main/AndroidManifest.xml | 1 + .../channels/ios/Runner/AppDelegate.m | 13 +- dev/integration_tests/channels/lib/main.dart | 2 + .../channels/lib/src/basic_messaging.dart | 36 ++++++ .../channels/lib/src/test_step.dart | 2 + .../flutter/lib/src/services/binding.dart | 15 +++ .../lib/src/services/platform_channel.dart | 120 ++++++++++++++++-- packages/flutter_test/lib/src/binding.dart | 3 + 8 files changed, 182 insertions(+), 10 deletions(-) diff --git a/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml index 6bf32c27294..d63de06817a 100644 --- a/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml +++ b/dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ found in the LICENSE file. --> Application and put your custom class here. --> { () => basicStringMessageToUnknownChannel(), () => basicJsonMessageToUnknownChannel(), () => basicStandardMessageToUnknownChannel(), + if (Platform.isIOS) + () => basicBackgroundStandardEcho(123), ]; Future? _result; int _step = 0; diff --git a/dev/integration_tests/channels/lib/src/basic_messaging.dart b/dev/integration_tests/channels/lib/src/basic_messaging.dart index 08392fc5d49..c7a41330ba2 100644 --- a/dev/integration_tests/channels/lib/src/basic_messaging.dart +++ b/dev/integration_tests/channels/lib/src/basic_messaging.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:isolate'; import 'package:flutter/services.dart'; @@ -78,6 +79,41 @@ Future basicStandardHandshake(dynamic message) async { 'Standard >${toString(message)}<', channel, message); } +Future _basicBackgroundStandardEchoMain(List args) async { + final SendPort sendPort = args[2] as SendPort; + final Object message = args[1]; + final String name = 'Background Echo >${toString(message)}<'; + const String description = + 'Uses a platform channel from a background isolate.'; + try { + BackgroundIsolateBinaryMessenger.ensureInitialized( + args[0] as RootIsolateToken); + const BasicMessageChannel channel = BasicMessageChannel( + 'std-echo', + ExtendedStandardMessageCodec(), + ); + final Object response = await channel.send(message) as Object; + + final TestStatus testStatus = TestStepResult.deepEquals(message, response) + ? TestStatus.ok + : TestStatus.failed; + sendPort.send(TestStepResult(name, description, testStatus)); + } catch (ex) { + sendPort.send(TestStepResult(name, description, TestStatus.failed, + error: ex.toString())); + } +} + +Future basicBackgroundStandardEcho(Object message) async { + final ReceivePort receivePort = ReceivePort(); + Isolate.spawn(_basicBackgroundStandardEchoMain, [ + ServicesBinding.instance.rootIsolateToken!, + message, + receivePort.sendPort, + ]); + return await receivePort.first as TestStepResult; +} + Future basicBinaryMessageToUnknownChannel() async { const BasicMessageChannel channel = BasicMessageChannel( diff --git a/dev/integration_tests/channels/lib/src/test_step.dart b/dev/integration_tests/channels/lib/src/test_step.dart index 6702560ebff..b449099f92c 100644 --- a/dev/integration_tests/channels/lib/src/test_step.dart +++ b/dev/integration_tests/channels/lib/src/test_step.dart @@ -90,6 +90,8 @@ class TestStepResult { ], ); } + + static bool deepEquals(dynamic a, dynamic b) => _deepEquals(a, b); } Future resultOfHandshake( diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 5fd8436b329..964238ffed3 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -82,6 +82,21 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { BinaryMessenger get defaultBinaryMessenger => _defaultBinaryMessenger; late final BinaryMessenger _defaultBinaryMessenger; + /// A token that represents the root isolate, used for coordinating with background + /// isolates. + /// + /// This property is primarily intended for use with + /// [BackgroundIsolateBinaryMessenger.ensureInitialized], which takes a + /// [RootIsolateToken] as its argument. The value `null` is returned when + /// executed from background isolates. + ui.RootIsolateToken? get rootIsolateToken => ui.RootIsolateToken.instance; + + /// Returns `true` if executed on a background (non-root) isolate. + /// + /// The value `false` will always be returned on web since there is no notion + /// of root/background isolates on the web. + bool get useBackgroundIsolateBinaryMessenger => !kIsWeb && rootIsolateToken == null; + /// The low level buffering and dispatch mechanism for messages sent by /// plugins on the engine side to their corresponding plugin code on /// the framework side. diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index 66f1e7e8e88..f7811c76b39 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:developer'; +import 'dart:isolate' show ReceivePort; +import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -13,6 +15,7 @@ import 'debug.dart' show debugProfilePlatformChannels; import 'message_codec.dart'; import 'message_codecs.dart'; +export 'dart:ui' show RootIsolateToken; export 'binary_messenger.dart' show BinaryMessenger; export 'message_codec.dart' show MessageCodec, MethodCall, MethodCodec; @@ -123,6 +126,93 @@ void _debugRecordDownStream(String channelTypeName, String name, _debugLaunchProfilePlatformChannels(); } +/// A [BinaryMessenger] for use on background (non-root) isolates. +class BackgroundIsolateBinaryMessenger extends BinaryMessenger { + BackgroundIsolateBinaryMessenger._(); + + final ReceivePort _receivePort = ReceivePort(); + final Map> _completers = >{}; + int _messageCount = 0; + + /// The existing instance of this class, if any. + /// + /// Throws if [ensureInitialized] has not been called at least once. + static BinaryMessenger get instance { + if (_instance == null) { + throw StateError( + 'The BackgroundIsolateBinaryMessenger.instance value is invalid ' + 'until BackgroundIsolateBinaryMessenger.ensureInitialized is ' + 'executed.'); + } + return _instance!; + } + + static BinaryMessenger? _instance; + + /// Ensures that [BackgroundIsolateBinaryMessenger.instance] has been initialized. + /// + /// The argument should be the value obtained from [ServicesBinding.rootIsolateToken] + /// on the root isolate. + /// + /// This function is idempotent (calling it multiple times is harmless but has no effect). + static void ensureInitialized(ui.RootIsolateToken token) { + if (_instance == null) { + ui.PlatformDispatcher.instance.registerBackgroundIsolate(token); + final BackgroundIsolateBinaryMessenger portBinaryMessenger = BackgroundIsolateBinaryMessenger._(); + _instance = portBinaryMessenger; + portBinaryMessenger._receivePort.listen((dynamic message) { + try { + final List args = message as List; + final int identifier = args[0] as int; + final Uint8List bytes = args[1] as Uint8List; + final ByteData byteData = ByteData.sublistView(bytes); + portBinaryMessenger._completers.remove(identifier)!.complete(byteData); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'services library', + context: + ErrorDescription('during a platform message response callback'), + )); + } + }); + } + } + + @override + Future handlePlatformMessage(String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback) { + throw UnimplementedError('handlePlatformMessage is deprecated.'); + } + + @override + Future? send(String channel, ByteData? message) { + final Completer completer = Completer(); + _messageCount += 1; + final int messageIdentifier = _messageCount; + _completers[messageIdentifier] = completer; + ui.PlatformDispatcher.instance.sendPortPlatformMessage( + channel, + message, + messageIdentifier, + _receivePort.sendPort, + ); + return completer.future; + } + + @override + void setMessageHandler(String channel, MessageHandler? handler) { + throw UnsupportedError( + 'Background isolates do not support setMessageHandler(). Messages from the host platform always go to the root isolate.'); + } +} + +BinaryMessenger _findBinaryMessenger() { + return ServicesBinding.instance.useBackgroundIsolateBinaryMessenger + ? BackgroundIsolateBinaryMessenger.instance + : ServicesBinding.instance.defaultBinaryMessenger; +} + /// A named channel for communicating with platform plugins using asynchronous /// message passing. /// @@ -160,10 +250,14 @@ class BasicMessageChannel { /// The message codec used by this channel, not null. final MessageCodec codec; - /// The messenger which sends the bytes for this channel, not null. + /// The messenger which sends the bytes for this channel. + /// + /// On the root isolate or web, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default + /// value is a [BackgroundIsolateBinaryMessenger] from + /// [BackgroundIsolateBinaryMessenger.ensureInitialized]. BinaryMessenger get binaryMessenger { - final BinaryMessenger result = - _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger(); return !kReleaseMode && debugProfilePlatformChannels ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( // ignore: no_runtimetype_tostring @@ -246,12 +340,14 @@ class MethodChannel { /// The message codec used by this channel, not null. final MethodCodec codec; - /// The messenger used by this channel to send platform messages. + /// The messenger which sends the bytes for this channel. /// - /// The messenger may not be null. + /// On the root isolate or web, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default + /// value is a [BackgroundIsolateBinaryMessenger] from + /// [BackgroundIsolateBinaryMessenger.ensureInitialized]. BinaryMessenger get binaryMessenger { - final BinaryMessenger result = - _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger(); return !kReleaseMode && debugProfilePlatformChannels ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( // ignore: no_runtimetype_tostring @@ -600,8 +696,14 @@ class EventChannel { /// The message codec used by this channel, not null. final MethodCodec codec; - /// The messenger used by this channel to send platform messages, not null. - BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + /// The messenger which sends the bytes for this channel. + /// + /// On the root isolate or web, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default + /// value is a [BackgroundIsolateBinaryMessenger] from + /// [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => + _binaryMessenger ?? _findBinaryMessenger(); final BinaryMessenger? _binaryMessenger; /// Sets up a broadcast stream for receiving events on this channel. diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 758b32ce0ab..cc4bb3765c1 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -104,6 +104,9 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding { TestDefaultBinaryMessenger createBinaryMessenger() { return TestDefaultBinaryMessenger(super.createBinaryMessenger()); } + + @override + bool get useBackgroundIsolateBinaryMessenger => false; } /// Base class for bindings used by widgets library tests.