Flutter Platform Channel
Flutter 官方提供了 Platform Channel 用于 Dart 侧与平台侧的数据通信,其主要数据链路如下。
MessageCodec 对数据进行编码,生成 Uint8List (Dart 对象,第一次拷贝)
-> 通过 native extension 将数据传给 Flutter Engine
-> 拆箱,获得 uint8_t* 的原始数据指针
-> 将数据复制到 std::vector<uint8_t> 中 (第二次拷贝)
-> 将数据 从UIRunner 传递到 PaltformRunner 中
Android 下平台链路如下
-> 创建 jbytearray
-> 将数据拷贝到 jbytearray 中(第三次拷贝)
-> JNI 传回,MessageCodec 解码(第四次拷贝)
-> 由 MethodCallHandler 处理
这种方式提供了一个相对不错的性能,能够满足大部分场景下的数据传递要求。但是从实现上看仍然存在一定的瓶颈(编解码,拷贝)。将不同长度的数据通过 MethodChannel 传递给 Android 原生后再原样传回,统计往返时间以度量性能情况,结果如下。
// Dart
var ts = DateTime.now();
for (var i = 0; i < count; i++) {
var cal = codec.encodeMethodCall(MethodCall('test', list));
codec.decodeMethodCall(cal);
}
print('编解码 ${DateTime.now().difference(ts).inMicroseconds}');
ts = DateTime.now();
for (var i = 0; i < count; i++) await platform.invokeMethod('benchmark', list);
print('Method Channel 调用 ${DateTime.now().difference(ts).inMicroseconds}');
// Kotlin
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
result.success(call.arguments)
}
重复次数 | 数据长度(字节) | 总编解码耗时(微秒) | 总耗时(微秒) | 单程通道耗时 |
10 | 1M | 4025 | 58894 | 2743 μs / MB |
20 | 1M | 8036 | 90813 | 2069 μs / MB |
30 | 1M | 11789 | 119261 | 1791 μs / MB |
20 | 512 K | 4143 | 43645 | 1975 μs / MB |
20 | 1 M | 8057 | 109885 | 2545 μs / MB |
20 | 5 M | 79848 | 360039 | 1400 μs / MB |
20 | 10 M | 145318 | 746307 | 1502 μs / MB |
20 | 20 M | 319748 | 1402876 | 1353 μs / MB |
20 | 40 M | 662206 | 2373947 | 1069 μs / MB |
从测试中可以看出,Platform Channel 的效率随着数据量大增大而提升,最终能够达到 1000 μs / MB 左右,由于没有考虑到平台侧的编解码耗时,这个实际效率应高于测试得到的效率。对于大量碎片化的小数据,Platform Channel 的效率较为低下。在传递图片等大数据量场景下,通道仍然会造成毫秒级的延迟,而当传输图片列表时,延迟会累加到上百毫秒甚至秒级,造成视觉上的卡顿,影响使用体验。
FFI 通道
FFI 是 Dart 2.7 引入的特性,它使 Dart VM 能够直接绑定和调用 C 函数。由于静态绑定和类型完备的特性,其理论性能应当优于官方通道的 native extension 实现。使用 FFI 模拟官方的 method channel 进行数据传递,测试结果如下。
// Dart
ts = DateTime.now();
for (var i = 0; i < count; i++) {
var ptr = allocate<Uint8>(count: message.length);
ptr.asTypedList(message.length).setAll(0, message);
var retPtr = nativeFunc(message.length, ptr);
var ret = retPtr.asTypedList(length);
free(ptr);
}
print('FFI 双向耗时 ${DateTime.now().difference(ts).inMicroseconds}');
// C++
uint8_t * NativeFunc(uint64_t length, uint8_t *data) {
…
JNIEnv *env = getEnv();
jbyteArray arr = env->NewByteArray(length);
env->SetByteArrayRegion(arr, 0, length, (jbyte *)data);
jobject obj = env->CallStaticObjectMethod(cls, mtd, arr);
jboolean copy = JNI_FALSE;
jbyte *ret = env->GetByteArrayElements(static_cast<jbyteArray>(obj), ©);
return (uint8_t *)ret;
}
// Kotlin
fun replyBytes(arr : ByteArray): ByteArray {
return arr
}
重复次数 | 数据长度(字节) | 总拷贝耗时(微秒) | 总耗时(微秒) | 单程通道耗时 |
20 | 512K | 25911 | 29574 | 183 μs / MB |
20 | 1M | 50866 | 60182 | 233 μs / MB |
20 | 5M | 255877 | 342361 | 432 μs / MB |
可以看出,FFI 通道的通道性能要远高于官方的 Platform Channel,效率随数据量的增大而降低。但是,由于使用 FFI 通道发送数据需在 Dart 分配 C 堆的内存空间并拷贝数据,相比于官方的在 C++ 层做拷贝的方式,效率大幅降低,造成了通道性能的大幅损失,使其最终结果已经接近官方的 Platform Channel。