Skip to content

Commit a7166e7

Browse files
authored
Reland isolate platform channels with conditional compilation (#111712)
1 parent cb8c725 commit a7166e7

File tree

10 files changed

+217
-11
lines changed

10 files changed

+217
-11
lines changed

dev/integration_tests/channels/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ found in the LICENSE file. -->
1818
Application and put your custom class here. -->
1919
<application android:name="${applicationName}" android:label="channels" android:icon="@mipmap/ic_launcher">
2020
<activity android:name=".MainActivity"
21+
android:exported="true"
2122
android:launchMode="singleTop"
2223
android:theme="@android:style/Theme.Black.NoTitleBar"
2324
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

dev/integration_tests/channels/android/app/src/main/java/com/yourcompany/channels/MainActivity.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
2929
setupMessageHandshake(new BasicMessageChannel<>(dartExecutor, "std-msg", ExtendedStandardMessageCodec.INSTANCE));
3030
setupMethodHandshake(new MethodChannel(dartExecutor, "json-method", JSONMethodCodec.INSTANCE));
3131
setupMethodHandshake(new MethodChannel(dartExecutor, "std-method", new StandardMethodCodec(ExtendedStandardMessageCodec.INSTANCE)));
32+
33+
BasicMessageChannel echoChannel =
34+
new BasicMessageChannel(dartExecutor, "std-echo", ExtendedStandardMessageCodec.INSTANCE);
35+
echoChannel.setMessageHandler(new BasicMessageChannel.MessageHandler(){
36+
@Override
37+
public void onMessage(final Object message, final BasicMessageChannel.Reply reply) {
38+
reply.reply(message);
39+
}
40+
});
3241
}
3342

3443
private <T> void setupMessageHandshake(final BasicMessageChannel<T> channel) {

dev/integration_tests/channels/ios/Runner/AppDelegate.m

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,18 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
114114
[FlutterMethodChannel methodChannelWithName:@"std-method"
115115
binaryMessenger:flutterController
116116
codec:[FlutterStandardMethodCodec codecWithReaderWriter:extendedReaderWriter]]];
117-
return [super application:application didFinishLaunchingWithOptions:launchOptions];
117+
118+
[[FlutterBasicMessageChannel
119+
messageChannelWithName:@"std-echo"
120+
binaryMessenger:flutterController
121+
codec:[FlutterStandardMessageCodec
122+
codecWithReaderWriter:extendedReaderWriter]]
123+
setMessageHandler:^(id message, FlutterReply reply) {
124+
reply(message);
125+
}];
126+
127+
return [super application:application
128+
didFinishLaunchingWithOptions:launchOptions];
118129
}
119130

120131
- (void)setupMessagingHandshakeOnChannel:(FlutterBasicMessageChannel*)channel {

dev/integration_tests/channels/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ class _TestAppState extends State<TestApp> {
173173
() => basicStringMessageToUnknownChannel(),
174174
() => basicJsonMessageToUnknownChannel(),
175175
() => basicStandardMessageToUnknownChannel(),
176+
if (Platform.isIOS || Platform.isAndroid)
177+
() => basicBackgroundStandardEcho(123),
176178
];
177179
Future<TestStepResult>? _result;
178180
int _step = 0;

dev/integration_tests/channels/lib/src/basic_messaging.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:isolate';
67

78
import 'package:flutter/services.dart';
89

@@ -78,6 +79,41 @@ Future<TestStepResult> basicStandardHandshake(dynamic message) async {
7879
'Standard >${toString(message)}<', channel, message);
7980
}
8081

82+
Future<void> _basicBackgroundStandardEchoMain(List<Object> args) async {
83+
final SendPort sendPort = args[2] as SendPort;
84+
final Object message = args[1];
85+
final String name = 'Background Echo >${toString(message)}<';
86+
const String description =
87+
'Uses a platform channel from a background isolate.';
88+
try {
89+
BackgroundIsolateBinaryMessenger.ensureInitialized(
90+
args[0] as RootIsolateToken);
91+
const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
92+
'std-echo',
93+
ExtendedStandardMessageCodec(),
94+
);
95+
final Object response = await channel.send(message) as Object;
96+
97+
final TestStatus testStatus = TestStepResult.deepEquals(message, response)
98+
? TestStatus.ok
99+
: TestStatus.failed;
100+
sendPort.send(TestStepResult(name, description, testStatus));
101+
} catch (ex) {
102+
sendPort.send(TestStepResult(name, description, TestStatus.failed,
103+
error: ex.toString()));
104+
}
105+
}
106+
107+
Future<TestStepResult> basicBackgroundStandardEcho(Object message) async {
108+
final ReceivePort receivePort = ReceivePort();
109+
Isolate.spawn(_basicBackgroundStandardEchoMain, <Object>[
110+
ServicesBinding.rootIsolateToken!,
111+
message,
112+
receivePort.sendPort,
113+
]);
114+
return await receivePort.first as TestStepResult;
115+
}
116+
81117
Future<TestStepResult> basicBinaryMessageToUnknownChannel() async {
82118
const BasicMessageChannel<ByteData?> channel =
83119
BasicMessageChannel<ByteData?>(

dev/integration_tests/channels/lib/src/test_step.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ class TestStepResult {
9090
],
9191
);
9292
}
93+
94+
static bool deepEquals(dynamic a, dynamic b) => _deepEquals(a, b);
9395
}
9496

9597
Future<TestStepResult> resultOfHandshake(
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async' show Completer;
6+
import 'dart:isolate' show ReceivePort;
7+
import 'dart:ui' as ui;
8+
9+
import 'package:flutter/foundation.dart';
10+
import 'binary_messenger.dart';
11+
import 'binding.dart';
12+
13+
/// A [BinaryMessenger] for use on background (non-root) isolates.
14+
class BackgroundIsolateBinaryMessenger extends BinaryMessenger {
15+
BackgroundIsolateBinaryMessenger._();
16+
17+
final ReceivePort _receivePort = ReceivePort();
18+
final Map<int, Completer<ByteData?>> _completers =
19+
<int, Completer<ByteData?>>{};
20+
int _messageCount = 0;
21+
22+
/// The existing instance of this class, if any.
23+
///
24+
/// Throws if [ensureInitialized] has not been called at least once.
25+
static BinaryMessenger get instance {
26+
if (_instance == null) {
27+
throw StateError(
28+
'The BackgroundIsolateBinaryMessenger.instance value is invalid '
29+
'until BackgroundIsolateBinaryMessenger.ensureInitialized is '
30+
'executed.');
31+
}
32+
return _instance!;
33+
}
34+
35+
static BinaryMessenger? _instance;
36+
37+
/// Ensures that [BackgroundIsolateBinaryMessenger.instance] has been initialized.
38+
///
39+
/// The argument should be the value obtained from [ServicesBinding.rootIsolateToken]
40+
/// on the root isolate.
41+
///
42+
/// This function is idempotent (calling it multiple times is harmless but has no effect).
43+
static void ensureInitialized(ui.RootIsolateToken token) {
44+
if (_instance == null) {
45+
ui.PlatformDispatcher.instance.registerBackgroundIsolate(token);
46+
final BackgroundIsolateBinaryMessenger portBinaryMessenger =
47+
BackgroundIsolateBinaryMessenger._();
48+
_instance = portBinaryMessenger;
49+
portBinaryMessenger._receivePort.listen((dynamic message) {
50+
try {
51+
final List<dynamic> args = message as List<dynamic>;
52+
final int identifier = args[0] as int;
53+
final Uint8List bytes = args[1] as Uint8List;
54+
final ByteData byteData = ByteData.sublistView(bytes);
55+
portBinaryMessenger._completers
56+
.remove(identifier)!
57+
.complete(byteData);
58+
} catch (exception, stack) {
59+
FlutterError.reportError(FlutterErrorDetails(
60+
exception: exception,
61+
stack: stack,
62+
library: 'services library',
63+
context:
64+
ErrorDescription('during a platform message response callback'),
65+
));
66+
}
67+
});
68+
}
69+
}
70+
71+
@override
72+
Future<void> handlePlatformMessage(String channel, ByteData? data,
73+
ui.PlatformMessageResponseCallback? callback) {
74+
throw UnimplementedError('handlePlatformMessage is deprecated.');
75+
}
76+
77+
@override
78+
Future<ByteData?>? send(String channel, ByteData? message) {
79+
final Completer<ByteData?> completer = Completer<ByteData?>();
80+
_messageCount += 1;
81+
final int messageIdentifier = _messageCount;
82+
_completers[messageIdentifier] = completer;
83+
ui.PlatformDispatcher.instance.sendPortPlatformMessage(
84+
channel,
85+
message,
86+
messageIdentifier,
87+
_receivePort.sendPort,
88+
);
89+
return completer.future;
90+
}
91+
92+
@override
93+
void setMessageHandler(String channel, MessageHandler? handler) {
94+
throw UnsupportedError(
95+
'Background isolates do not support setMessageHandler(). Messages from the host platform always go to the root isolate.');
96+
}
97+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'binding.dart';
6+
7+
// ignore: avoid_classes_with_only_static_members
8+
/// Stand-in for non-web platforms' [BackgroundIsolateBinaryMessenger].
9+
class BackgroundIsolateBinaryMessenger {
10+
/// Throws an [UnsupportedError].
11+
static BinaryMessenger get instance {
12+
throw UnsupportedError('Isolates not supported on web.');
13+
}
14+
}

packages/flutter/lib/src/services/binding.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import 'service_extensions.dart';
1919
import 'system_channels.dart';
2020
import 'text_input.dart';
2121

22-
export 'dart:ui' show ChannelBuffers;
22+
export 'dart:ui' show ChannelBuffers, RootIsolateToken;
2323

2424
export 'binary_messenger.dart' show BinaryMessenger;
2525
export 'hardware_keyboard.dart' show HardwareKeyboard, KeyEventManager;
@@ -83,6 +83,15 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
8383
BinaryMessenger get defaultBinaryMessenger => _defaultBinaryMessenger;
8484
late final BinaryMessenger _defaultBinaryMessenger;
8585

86+
/// A token that represents the root isolate, used for coordinating with background
87+
/// isolates.
88+
///
89+
/// This property is primarily intended for use with
90+
/// [BackgroundIsolateBinaryMessenger.ensureInitialized], which takes a
91+
/// [RootIsolateToken] as its argument. The value `null` is returned when
92+
/// executed from background isolates.
93+
static ui.RootIsolateToken? get rootIsolateToken => ui.RootIsolateToken.instance;
94+
8695
/// The low level buffering and dispatch mechanism for messages sent by
8796
/// plugins on the engine side to their corresponding plugin code on
8897
/// the framework side.

packages/flutter/lib/src/services/platform_channel.dart

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@ import 'dart:developer';
77

88
import 'package:flutter/foundation.dart';
99

10+
import '_background_isolate_binary_messenger_io.dart'
11+
if (dart.library.html) '_background_isolate_binary_messenger_web.dart';
12+
1013
import 'binary_messenger.dart';
1114
import 'binding.dart';
1215
import 'debug.dart' show debugProfilePlatformChannels;
1316
import 'message_codec.dart';
1417
import 'message_codecs.dart';
1518

19+
export '_background_isolate_binary_messenger_io.dart'
20+
if (dart.library.html) '_background_isolate_binary_messenger_web.dart';
21+
1622
export 'binary_messenger.dart' show BinaryMessenger;
23+
export 'binding.dart' show RootIsolateToken;
1724
export 'message_codec.dart' show MessageCodec, MethodCall, MethodCodec;
1825

1926
bool _debugProfilePlatformChannelsIsRunning = false;
@@ -123,6 +130,12 @@ void _debugRecordDownStream(String channelTypeName, String name,
123130
_debugLaunchProfilePlatformChannels();
124131
}
125132

133+
BinaryMessenger _findBinaryMessenger() {
134+
return !kIsWeb && ServicesBinding.rootIsolateToken == null
135+
? BackgroundIsolateBinaryMessenger.instance
136+
: ServicesBinding.instance.defaultBinaryMessenger;
137+
}
138+
126139
/// A named channel for communicating with platform plugins using asynchronous
127140
/// message passing.
128141
///
@@ -160,10 +173,14 @@ class BasicMessageChannel<T> {
160173
/// The message codec used by this channel, not null.
161174
final MessageCodec<T> codec;
162175

163-
/// The messenger which sends the bytes for this channel, not null.
176+
/// The messenger which sends the bytes for this channel.
177+
///
178+
/// On the root isolate or web, this defaults to the
179+
/// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
180+
/// value is a [BackgroundIsolateBinaryMessenger] from
181+
/// [BackgroundIsolateBinaryMessenger.ensureInitialized].
164182
BinaryMessenger get binaryMessenger {
165-
final BinaryMessenger result =
166-
_binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger;
183+
final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger();
167184
return !kReleaseMode && debugProfilePlatformChannels
168185
? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger(
169186
// ignore: no_runtimetype_tostring
@@ -246,12 +263,14 @@ class MethodChannel {
246263
/// The message codec used by this channel, not null.
247264
final MethodCodec codec;
248265

249-
/// The messenger used by this channel to send platform messages.
266+
/// The messenger which sends the bytes for this channel.
250267
///
251-
/// The messenger may not be null.
268+
/// On the root isolate or web, this defaults to the
269+
/// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
270+
/// value is a [BackgroundIsolateBinaryMessenger] from
271+
/// [BackgroundIsolateBinaryMessenger.ensureInitialized].
252272
BinaryMessenger get binaryMessenger {
253-
final BinaryMessenger result =
254-
_binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger;
273+
final BinaryMessenger result = _binaryMessenger ?? _findBinaryMessenger();
255274
return !kReleaseMode && debugProfilePlatformChannels
256275
? _debugBinaryMessengers[this] ??= _ProfiledBinaryMessenger(
257276
// ignore: no_runtimetype_tostring
@@ -600,8 +619,14 @@ class EventChannel {
600619
/// The message codec used by this channel, not null.
601620
final MethodCodec codec;
602621

603-
/// The messenger used by this channel to send platform messages, not null.
604-
BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance.defaultBinaryMessenger;
622+
/// The messenger which sends the bytes for this channel.
623+
///
624+
/// On the root isolate or web, this defaults to the
625+
/// [ServicesBinding.defaultBinaryMessenger]. In other contexts the default
626+
/// value is a [BackgroundIsolateBinaryMessenger] from
627+
/// [BackgroundIsolateBinaryMessenger.ensureInitialized].
628+
BinaryMessenger get binaryMessenger =>
629+
_binaryMessenger ?? _findBinaryMessenger();
605630
final BinaryMessenger? _binaryMessenger;
606631

607632
/// Sets up a broadcast stream for receiving events on this channel.

0 commit comments

Comments
 (0)