diff --git a/dev/integration_tests/channels/ios/Runner/AppDelegate.m b/dev/integration_tests/channels/ios/Runner/AppDelegate.m index 719abd3a48835..8fc53c3005ce7 100644 --- a/dev/integration_tests/channels/ios/Runner/AppDelegate.m +++ b/dev/integration_tests/channels/ios/Runner/AppDelegate.m @@ -114,7 +114,18 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [FlutterMethodChannel methodChannelWithName:@"std-method" binaryMessenger:flutterController codec:[FlutterStandardMethodCodec codecWithReaderWriter:extendedReaderWriter]]]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + + [[FlutterBasicMessageChannel + messageChannelWithName:@"std-echo" + binaryMessenger:flutterController + codec:[FlutterStandardMessageCodec + codecWithReaderWriter:extendedReaderWriter]] + setMessageHandler:^(id message, FlutterReply reply) { + reply(message); + }]; + + return [super application:application + didFinishLaunchingWithOptions:launchOptions]; } - (void)setupMessagingHandshakeOnChannel:(FlutterBasicMessageChannel*)channel { diff --git a/dev/integration_tests/channels/lib/main.dart b/dev/integration_tests/channels/lib/main.dart index fdd1a25c583d5..70bfec9d4254d 100644 --- a/dev/integration_tests/channels/lib/main.dart +++ b/dev/integration_tests/channels/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -166,6 +167,8 @@ class _TestAppState extends State { () => 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 08392fc5d4912..03c24e1567621 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,40 @@ 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); + final BasicMessageChannel channel = BasicMessageChannel( + 'std-echo', + const ExtendedStandardMessageCodec(), + binaryMessenger: BackgroundIsolateBinaryMessenger.instance, + ); + 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 6702560ebff5b..b449099f92c5d 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 5fd8436b32930..22354e321233c 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -34,6 +34,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { @override void initInstances() { super.initInstances(); + assert(platformDispatcher.isRootIsolate, 'Bindings are only meaningful on the root isolate.'); _instance = this; _defaultBinaryMessenger = createBinaryMessenger(); _restorationManager = createRestorationManager(); @@ -82,6 +83,14 @@ 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. + late RootIsolateToken rootIsolateToken = platformDispatcher.registerRootIsolate(); + /// 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 47b609b3de821..648cd8b35477c 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 show PlatformDispatcher, PlatformMessageResponseCallback; import 'package:flutter/foundation.dart'; @@ -123,6 +125,116 @@ void _debugRecordDownStream(String channelTypeName, String name, _debugLaunchProfilePlatformChannels(); } +/// The transport for platform messages on background isolates. +/// +/// To obtain an instance of this class, call +/// [BackgroundIsolateBinaryMessenger.ensureInitialized] using the token obtained +/// from the root isolate's [ServicesBinding.rootIsolateToken]. +/// +/// Background isolates do not support [BinaryMessenger.setMessageHandler], they +/// can only handle replies (one reply per message sent using [send]). +class BackgroundIsolateBinaryMessenger extends BinaryMessenger { + BackgroundIsolateBinaryMessenger._(RootIsolateToken rootIsolate) { + ui.PlatformDispatcher.instance.registerBackgroundIsolate(rootIsolate); + _receivePort.listen(_receive); + } + + /// The existing instance of this class, if any. + /// + /// Throws if [ensureInitialized] has not been called at least once. + static BackgroundIsolateBinaryMessenger get instance => _instance ?? (throw StateError( + 'BackgroundIsolateBinaryMessenger.instance must be initialized using BackgroundIsolateBinaryMessenger.ensureInitialized' + )); + static BackgroundIsolateBinaryMessenger? _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 BackgroundIsolateBinaryMessenger ensureInitialized(RootIsolateToken rootIsolate) { + return _instance ??= BackgroundIsolateBinaryMessenger._(rootIsolate); + } + + final ReceivePort _receivePort = ReceivePort(); + final Map> _completers = >{}; + int _messageCount = 0; + + @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; + } + + void _receive(Object? 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); + _completers.remove(identifier)!.complete(byteData); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'services library', + context: ErrorDescription('during a background isolate platform message response callback'), + )); + } + } + + @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(BinaryMessenger? explicitBinaryMessenger, Object client, Object codec) { + BinaryMessenger result; + if (explicitBinaryMessenger != null) { + result = explicitBinaryMessenger; + } else { + assert(() { + if (!ui.PlatformDispatcher.instance.isRootIsolate) { + throw ArgumentError.value( + explicitBinaryMessenger, + 'binaryMessenger', + 'On background isolates, the "binaryMessenger" argument to ${client.runtimeType} constructors ' + 'must be an explicit BinaryMessenger, not null. Typically a BackgroundIsolateBinaryMessenger ' + 'is used, as obtained from BackgroundIsolateBinaryMessenger.ensureInitialized().', + ); + } + return true; + }()); + result = ServicesBinding.instance.defaultBinaryMessenger; + } + assert(() { + if (debugProfilePlatformChannels) { + result = _debugBinaryMessengers[client] ??= _ProfiledBinaryMessenger( + result, + client.runtimeType.toString(), + codec.runtimeType.toString(), + ); + } + return true; + }()); + return result; +} + /// A named channel for communicating with platform plugins using asynchronous /// message passing. /// @@ -147,8 +259,10 @@ void _debugRecordDownStream(String channelTypeName, String name, class BasicMessageChannel { /// Creates a [BasicMessageChannel] with the specified [name], [codec] and [binaryMessenger]. /// - /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// The [name] and [codec] arguments cannot be null. The default + /// [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null on the root isolate; on other isolates it must + /// not be null. const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger }) : assert(name != null), assert(codec != null), @@ -161,15 +275,14 @@ class BasicMessageChannel { final MessageCodec codec; /// The messenger which sends the bytes for this channel, not null. - BinaryMessenger get binaryMessenger { - final BinaryMessenger result = - _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; - return !kReleaseMode && debugProfilePlatformChannels - ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( - // ignore: no_runtimetype_tostring - result, runtimeType.toString(), codec.runtimeType.toString()) - : result; - } + /// + /// On the root isolate, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. + /// + /// On other isolates, a [BinaryMessenger] must be specified in the + /// [BasicMessageChannel] constructor. Typically this is the transport + /// obtained using [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => _findBinaryMessenger(_binaryMessenger, this, codec); final BinaryMessenger? _binaryMessenger; /// Sends the specified [message] to the platform plugins on this channel. @@ -233,8 +346,10 @@ class MethodChannel { /// The [codec] used will be [StandardMethodCodec], unless otherwise /// specified. /// - /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// The [name] and [codec] arguments cannot be null. The default + /// [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null on the root isolate; on other isolates it must + /// not be null. const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ]) : assert(name != null), assert(codec != null), @@ -248,16 +363,13 @@ class MethodChannel { /// The messenger used by this channel to send platform messages. /// - /// The messenger may not be null. - BinaryMessenger get binaryMessenger { - final BinaryMessenger result = - _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; - return !kReleaseMode && debugProfilePlatformChannels - ? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger( - // ignore: no_runtimetype_tostring - result, runtimeType.toString(), codec.runtimeType.toString()) - : result; - } + /// On the root isolate, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. + /// + /// On other isolates, a [BinaryMessenger] must be specified in the + /// [MethodChannel] constructor. Typically this is the transport + /// obtained using [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => _findBinaryMessenger(_binaryMessenger, this, codec); final BinaryMessenger? _binaryMessenger; /// Backend implementation of [invokeMethod]. @@ -587,8 +699,10 @@ class EventChannel { /// The [codec] used will be [StandardMethodCodec], unless otherwise /// specified. /// - /// Neither [name] nor [codec] may be null. The default [ServicesBinding.defaultBinaryMessenger] - /// instance is used if [binaryMessenger] is null. + /// Neither [name] nor [codec] may be null. The default + /// [ServicesBinding.defaultBinaryMessenger] instance is used if + /// [binaryMessenger] is null on the root isolate; on other isolates it must + /// not be null. const EventChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger]) : assert(name != null), assert(codec != null), @@ -601,7 +715,14 @@ class EventChannel { final MethodCodec codec; /// The messenger used by this channel to send platform messages, not null. - BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger; + /// + /// On the root isolate, this defaults to the + /// [ServicesBinding.defaultBinaryMessenger]. + /// + /// On other isolates, a [BinaryMessenger] must be specified in the + /// [EventChannel] constructor. Typically this is the transport + /// obtained using [BackgroundIsolateBinaryMessenger.ensureInitialized]. + BinaryMessenger get binaryMessenger => _findBinaryMessenger(_binaryMessenger, this, codec); final BinaryMessenger? _binaryMessenger; /// Sets up a broadcast stream for receiving events on this channel.