Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 6e3d533

Browse files
authored
Reland: "android platform channels: moved to direct buffers for c <-> java interop" (#26515)
1 parent c1fe7fb commit 6e3d533

File tree

11 files changed

+166
-29
lines changed

11 files changed

+166
-29
lines changed

shell/platform/android/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ action("robolectric_tests") {
487487
"test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java",
488488
"test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java",
489489
"test/io/flutter/external/FlutterLaunchTests.java",
490+
"test/io/flutter/plugin/common/BinaryCodecTest.java",
490491
"test/io/flutter/plugin/common/StandardMessageCodecTest.java",
491492
"test/io/flutter/plugin/common/StandardMethodCodecTest.java",
492493
"test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java",

shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformM
814814
@SuppressWarnings("unused")
815815
@VisibleForTesting
816816
public void handlePlatformMessage(
817-
@NonNull final String channel, byte[] message, final int replyId) {
817+
@NonNull final String channel, ByteBuffer message, final int replyId) {
818818
if (platformMessageHandler != null) {
819819
platformMessageHandler.handleMessageFromDart(channel, message, replyId);
820820
}
@@ -825,7 +825,7 @@ public void handlePlatformMessage(
825825
// Called by native to respond to a platform message that we sent.
826826
// TODO(mattcarroll): determine if reply is nonull or nullable
827827
@SuppressWarnings("unused")
828-
private void handlePlatformMessageResponse(int replyId, byte[] reply) {
828+
private void handlePlatformMessageResponse(int replyId, ByteBuffer reply) {
829829
if (platformMessageHandler != null) {
830830
platformMessageHandler.handlePlatformMessageResponse(replyId, reply);
831831
}

shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,18 @@ public void send(
7575

7676
@Override
7777
public void handleMessageFromDart(
78-
@NonNull final String channel, @Nullable byte[] message, final int replyId) {
78+
@NonNull final String channel, @Nullable ByteBuffer message, final int replyId) {
7979
Log.v(TAG, "Received message from Dart over channel '" + channel + "'");
8080
BinaryMessenger.BinaryMessageHandler handler = messageHandlers.get(channel);
8181
if (handler != null) {
8282
try {
8383
Log.v(TAG, "Deferring to registered handler to process message.");
84-
final ByteBuffer buffer = (message == null ? null : ByteBuffer.wrap(message));
85-
handler.onMessage(buffer, new Reply(flutterJNI, replyId));
84+
handler.onMessage(message, new Reply(flutterJNI, replyId));
85+
if (message != null && message.isDirect()) {
86+
// This ensures that if a user retains an instance to the ByteBuffer and it happens to
87+
// be direct they will get a deterministic error.
88+
message.limit(0);
89+
}
8690
} catch (Exception ex) {
8791
Log.e(TAG, "Uncaught exception in binary message listener", ex);
8892
flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId);
@@ -96,13 +100,18 @@ public void handleMessageFromDart(
96100
}
97101

98102
@Override
99-
public void handlePlatformMessageResponse(int replyId, @Nullable byte[] reply) {
103+
public void handlePlatformMessageResponse(int replyId, @Nullable ByteBuffer reply) {
100104
Log.v(TAG, "Received message reply from Dart.");
101105
BinaryMessenger.BinaryReply callback = pendingReplies.remove(replyId);
102106
if (callback != null) {
103107
try {
104108
Log.v(TAG, "Invoking registered callback for reply from Dart.");
105-
callback.reply(reply == null ? null : ByteBuffer.wrap(reply));
109+
callback.reply(reply);
110+
if (reply != null && reply.isDirect()) {
111+
// This ensures that if a user retains an instance to the ByteBuffer and it happens to
112+
// be direct they will get a deterministic error.
113+
reply.limit(0);
114+
}
106115
} catch (Exception ex) {
107116
Log.e(TAG, "Uncaught exception in binary message reply handler", ex);
108117
} catch (Error err) {

shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
import androidx.annotation.NonNull;
88
import androidx.annotation.Nullable;
9+
import java.nio.ByteBuffer;
910

1011
/** Handler that receives messages from Dart code. */
1112
public interface PlatformMessageHandler {
1213
void handleMessageFromDart(
13-
@NonNull final String channel, @Nullable byte[] message, final int replyId);
14+
@NonNull final String channel, @Nullable ByteBuffer message, final int replyId);
1415

15-
void handlePlatformMessageResponse(int replyId, @Nullable byte[] reply);
16+
void handlePlatformMessageResponse(int replyId, @Nullable ByteBuffer reply);
1617
}

shell/platform/android/io/flutter/plugin/common/BinaryCodec.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package io.flutter.plugin.common;
66

7+
import androidx.annotation.Nullable;
78
import java.nio.ByteBuffer;
89

910
/**
@@ -18,16 +19,48 @@
1819
public final class BinaryCodec implements MessageCodec<ByteBuffer> {
1920
// This codec must match the Dart codec of the same name in package flutter/services.
2021
public static final BinaryCodec INSTANCE = new BinaryCodec();
22+
/**
23+
* A BinaryCodec that returns direct ByteBuffers from `decodeMessage` for better performance.
24+
*
25+
* @see BinaryCodec.BinaryCodec(boolean)
26+
*/
27+
public static final BinaryCodec INSTANCE_DIRECT = new BinaryCodec(true);
2128

22-
private BinaryCodec() {}
29+
private final boolean returnsDirectByteBufferFromDecoding;
30+
31+
private BinaryCodec() {
32+
this.returnsDirectByteBufferFromDecoding = false;
33+
}
34+
35+
/**
36+
* A constructor for BinaryCodec.
37+
*
38+
* @param returnsDirectByteBufferFromDecoding `true` means that the Codec will return direct
39+
* ByteBuffers from `decodeMessage`. Direct ByteBuffers will have better performance but will
40+
* be invalid beyond the scope of the `decodeMessage` call. `false` means Flutter will copy
41+
* the encoded message to Java's memory, so the ByteBuffer will be valid beyond the
42+
* decodeMessage call, at the cost of a copy.
43+
*/
44+
private BinaryCodec(boolean returnsDirectByteBufferFromDecoding) {
45+
this.returnsDirectByteBufferFromDecoding = returnsDirectByteBufferFromDecoding;
46+
}
2347

2448
@Override
25-
public ByteBuffer encodeMessage(ByteBuffer message) {
49+
public ByteBuffer encodeMessage(@Nullable ByteBuffer message) {
2650
return message;
2751
}
2852

2953
@Override
30-
public ByteBuffer decodeMessage(ByteBuffer message) {
31-
return message;
54+
public ByteBuffer decodeMessage(@Nullable ByteBuffer message) {
55+
if (message == null) {
56+
return message;
57+
} else if (returnsDirectByteBufferFromDecoding) {
58+
return message;
59+
} else {
60+
ByteBuffer result = ByteBuffer.allocate(message.capacity());
61+
result.put(message);
62+
result.rewind();
63+
return result;
64+
}
3265
}
3366
}

shell/platform/android/io/flutter/plugin/common/MessageCodec.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public interface MessageCodec<T> {
2626
/**
2727
* Decodes the specified message from binary.
2828
*
29+
* <p><b>Warning:</b> The ByteBuffer is "direct" and it won't be valid beyond this call. Storing
30+
* the ByteBuffer and using it later and will lead to a {@code java.nio.BufferUnderflowException}.
31+
* If you want to retain the data you'll need to copy it.
32+
*
2933
* @param message the {@link ByteBuffer} message, possibly null.
3034
* @return a T value representation of the bytes between the given buffer's current position and
3135
* its limit, or null, if message is null.

shell/platform/android/platform_view_android_jni_impl.cc

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -828,15 +828,16 @@ bool RegisterApi(JNIEnv* env) {
828828

829829
g_handle_platform_message_method =
830830
env->GetMethodID(g_flutter_jni_class->obj(), "handlePlatformMessage",
831-
"(Ljava/lang/String;[BI)V");
831+
"(Ljava/lang/String;Ljava/nio/ByteBuffer;I)V");
832832

833833
if (g_handle_platform_message_method == nullptr) {
834834
FML_LOG(ERROR) << "Could not locate handlePlatformMessage method";
835835
return false;
836836
}
837837

838838
g_handle_platform_message_response_method = env->GetMethodID(
839-
g_flutter_jni_class->obj(), "handlePlatformMessageResponse", "(I[B)V");
839+
g_flutter_jni_class->obj(), "handlePlatformMessageResponse",
840+
"(ILjava/nio/ByteBuffer;)V");
840841

841842
if (g_handle_platform_message_response_method == nullptr) {
842843
FML_LOG(ERROR) << "Could not locate handlePlatformMessageResponse method";
@@ -1107,11 +1108,10 @@ void PlatformViewAndroidJNIImpl::FlutterViewHandlePlatformMessage(
11071108
fml::jni::StringToJavaString(env, message->channel());
11081109

11091110
if (message->hasData()) {
1110-
fml::jni::ScopedJavaLocalRef<jbyteArray> message_array(
1111-
env, env->NewByteArray(message->data().GetSize()));
1112-
env->SetByteArrayRegion(
1113-
message_array.obj(), 0, message->data().GetSize(),
1114-
reinterpret_cast<const jbyte*>(message->data().GetMapping()));
1111+
fml::jni::ScopedJavaLocalRef<jobject> message_array(
1112+
env, env->NewDirectByteBuffer(
1113+
const_cast<uint8_t*>(message->data().GetMapping()),
1114+
message->data().GetSize()));
11151115
env->CallVoidMethod(java_object.obj(), g_handle_platform_message_method,
11161116
java_channel.obj(), message_array.obj(), responseId);
11171117
} else {
@@ -1141,10 +1141,9 @@ void PlatformViewAndroidJNIImpl::FlutterViewHandlePlatformMessageResponse(
11411141
nullptr);
11421142
} else {
11431143
// Convert the vector to a Java byte array.
1144-
fml::jni::ScopedJavaLocalRef<jbyteArray> data_array(
1145-
env, env->NewByteArray(data->GetSize()));
1146-
env->SetByteArrayRegion(data_array.obj(), 0, data->GetSize(),
1147-
reinterpret_cast<const jbyte*>(data->GetMapping()));
1144+
fml::jni::ScopedJavaLocalRef<jobject> data_array(
1145+
env, env->NewDirectByteBuffer(const_cast<uint8_t*>(data->GetMapping()),
1146+
data->GetSize()));
11481147

11491148
env->CallVoidMethod(java_object.obj(),
11501149
g_handle_platform_message_response_method, responseId,

shell/platform/android/test/io/flutter/FlutterTestSuite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
3131
import io.flutter.embedding.engine.systemchannels.RestorationChannelTest;
3232
import io.flutter.external.FlutterLaunchTests;
33+
import io.flutter.plugin.common.BinaryCodecTest;
3334
import io.flutter.plugin.common.StandardMessageCodecTest;
3435
import io.flutter.plugin.common.StandardMethodCodecTest;
3536
import io.flutter.plugin.editing.InputConnectionAdaptorTest;
@@ -53,6 +54,7 @@
5354
AccessibilityBridgeTest.class,
5455
AndroidKeyProcessorTest.class,
5556
ApplicationInfoLoaderTest.class,
57+
BinaryCodecTest.class,
5658
DartExecutorTest.class,
5759
DartMessengerTest.class,
5860
FlutterActivityAndFragmentDelegateTest.class,

shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package io.flutter.embedding.engine.dart;
22

3+
import static junit.framework.TestCase.assertEquals;
34
import static junit.framework.TestCase.assertNotNull;
45
import static junit.framework.TestCase.assertTrue;
56
import static org.mockito.Matchers.any;
67
import static org.mockito.Mockito.mock;
78

89
import io.flutter.embedding.engine.FlutterJNI;
10+
import io.flutter.plugin.common.BinaryMessenger;
911
import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler;
1012
import java.nio.ByteBuffer;
1113
import org.junit.Test;
@@ -46,9 +48,80 @@ public void itHandlesErrors() {
4648
.onMessage(any(ByteBuffer.class), any(DartMessenger.Reply.class));
4749

4850
messenger.setMessageHandler("test", throwingHandler);
49-
messenger.handleMessageFromDart("test", new byte[] {}, 0);
51+
messenger.handleMessageFromDart("test", ByteBuffer.allocate(0), 0);
5052
assertNotNull(reportingHandler.latestException);
5153
assertTrue(reportingHandler.latestException instanceof AssertionError);
5254
currentThread.setUncaughtExceptionHandler(savedHandler);
5355
}
56+
57+
@Test
58+
public void givesDirectByteBuffer() {
59+
// Setup test.
60+
final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class);
61+
final DartMessenger messenger = new DartMessenger(fakeFlutterJni);
62+
final String channel = "foobar";
63+
final boolean[] wasDirect = {false};
64+
final BinaryMessenger.BinaryMessageHandler handler =
65+
(message, reply) -> {
66+
wasDirect[0] = message.isDirect();
67+
};
68+
messenger.setMessageHandler(channel, handler);
69+
final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2);
70+
message.rewind();
71+
message.putChar('a');
72+
message.putChar('b');
73+
message.putChar('c');
74+
message.putChar('d');
75+
messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123);
76+
assertTrue(wasDirect[0]);
77+
}
78+
79+
@Test
80+
public void directByteBufferLimitZeroAfterUsage() {
81+
// Setup test.
82+
final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class);
83+
final DartMessenger messenger = new DartMessenger(fakeFlutterJni);
84+
final String channel = "foobar";
85+
final ByteBuffer[] byteBuffers = {null};
86+
final int bufferSize = 4 * 2;
87+
final BinaryMessenger.BinaryMessageHandler handler =
88+
(message, reply) -> {
89+
byteBuffers[0] = message;
90+
assertEquals(bufferSize, byteBuffers[0].limit());
91+
};
92+
messenger.setMessageHandler(channel, handler);
93+
final ByteBuffer message = ByteBuffer.allocateDirect(bufferSize);
94+
message.rewind();
95+
message.putChar('a');
96+
message.putChar('b');
97+
message.putChar('c');
98+
message.putChar('d');
99+
messenger.handleMessageFromDart(channel, message, /*replyId=*/ 123);
100+
assertNotNull(byteBuffers[0]);
101+
assertTrue(byteBuffers[0].isDirect());
102+
assertEquals(0, byteBuffers[0].limit());
103+
}
104+
105+
@Test
106+
public void directByteBufferLimitZeroAfterReply() {
107+
// Setup test.
108+
final FlutterJNI fakeFlutterJni = mock(FlutterJNI.class);
109+
final DartMessenger messenger = new DartMessenger(fakeFlutterJni);
110+
final ByteBuffer message = ByteBuffer.allocateDirect(4 * 2);
111+
final String channel = "foobar";
112+
message.rewind();
113+
message.putChar('a');
114+
message.putChar('b');
115+
message.putChar('c');
116+
message.putChar('d');
117+
final ByteBuffer[] byteBuffers = {null};
118+
BinaryMessenger.BinaryReply callback =
119+
(reply) -> {
120+
assertTrue(reply.isDirect());
121+
byteBuffers[0] = reply;
122+
};
123+
messenger.send(channel, null, callback);
124+
messenger.handlePlatformMessageResponse(1, message);
125+
assertEquals(0, byteBuffers[0].limit());
126+
}
54127
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.flutter.plugin.common;
2+
3+
import static org.junit.Assert.assertNull;
4+
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.robolectric.RobolectricTestRunner;
8+
import org.robolectric.annotation.Config;
9+
10+
@Config(manifest = Config.NONE)
11+
@RunWith(RobolectricTestRunner.class)
12+
public class BinaryCodecTest {
13+
@Test
14+
public void decodeNull() {
15+
assertNull(BinaryCodec.INSTANCE.decodeMessage(null));
16+
}
17+
}

0 commit comments

Comments
 (0)