Flutter Channel 是一个异步调用通道,如果想在 Dart 侧同步获取到 Native 返回的结果,调用的时候加上 await 就可以了:
- final int result = await platform.invokeMethod('hello channel');
所以这篇文章到此为止了?
不!上面这行代码其实是个『假同步』,因为它只保证了 Dart 代码的同步执行,而 Native 代码与 Dart 并不在同一条线程执行。试想下,如果你通过 Flutter Channel 打日志,但由于打日志的消息是异步传递到 Native 的,最后日志顺序可能是错的。而通过日志来排查一些时序性相关的 Bug 时,日志的顺序很重要。
因为 Flutter Channel 设计之初就是异步的,使用 await 来回切换线程所带来的开销不小。而且协程的 await 语法具有传递性,上层调用方也需要使用 await,层层传递。
而 DartNative (https://github.com/dart-native/dart_native) 设计之初就是同步调用的,且也支持异步调用:
- // new DNTest instance and call hello method.
- DNTest().hello('DartNative');
Why DartNative?
DartNative 是『真同步』,保证了执行顺序。同时也支持异步调用。
一行代码实现同步调用,告别 Flutter Channel 胶水代码带来的开发成本。
同步调用性能是 Flutter Channel 的数倍。分别使用 Flutter Channel 和 DartNative 调用 fooNSString: 方法,耗时相差三到四倍。性能数据可能在不同场景下有波动,可以通过执行 Benchmark 代码 来对比结果。
实现原理
下图以 Dart 同步调用 iOS Objective-C API 为例,描述了 DartNative 同步调用的原理。以一个字符串参数为例,讲述了从 Dart String 自动转为 Objective-C NSString 并传递给 hello: 方法的过程。返回值也是自动转换类型的,由于篇幅原因没在图片中描述。
在实现了基本的同步调用后,开发重点也转向了性能优化。
方法签名的优化
在 Dart 同步调用 Native 时,为了实现跨语言调用时参数和返回值类型的自动转换,需要先获取到 Native 的方法签名。这里做了两方面的性能优化:
- 通过 DartFFI 调用 OC Runtime 获取方法签名占据了一定耗时。可以在 Dart 侧加一层 Cache 来减少通信和反射次数。
- 方法签名字符串的构成是 “TypeEncoding+offset” 的组合,跨语言之间传递字符串的编解码的耗时较多,而只有 TypeEncoding 那部分才是类型自动转换所需要的。绝大部分类型对应的 TypeEncoding 都是固定的,于是只需要传递 TypeEncoding 的指针即可。
字符串转换的优化
Dart String 在与 Objective-C NSString 相互转换的过程中,数据传输的格式的选择至关重要。因为 Dart String 是使用 UTF16 编码的,所以 DartNative 使用 Uint16List 作为数据传输的格式。通过性能测试,使用 UTF16 来回传输字符串的总耗时(包含 Native 方法自身耗时)相比 UTF8 减少了 35% 左右,如果只计算通道自动类型转换耗时减少的比例会更多。
转换 Dart String 为 Objective-C NSString:
使用 DartFFI 在堆上创建 uint16_t 数组,将 Dart String 转为 UTF16 格式后装载进去。最终通过 perform 方法反射调用 stringWithCharacters:length: 方法来创建 NSString 对象。
- final units = value.codeUnits;
- final Pointer<Uint16> charPtr = allocate<Uint16>(count: units.length + 1);
- final Uint16List nativeString = charPtr.asTypedList(units.length + 1);
- nativeString.setAll(0, units);
- nativeString[units.length] = 0;
- NSObject result = Class('NSString').perform(
- SEL('stringWithCharacters:length:'),
- args: [charPtr, units.length]);
- free(charPtr);
转换 Objective-C NSString 为 Dart String:
NSString 转为 UTF16 稍微麻烦一点。这里的方案是先转为 UTF16 的 NSData,然后将 uint16_t 数组的地址和字符长度(不是字节长度)返回给 Dart 侧。
- const void *
- native_convert_nsstring_to_utf16(NSString *string, NSUInteger *length) {
- NSData *data = [string dataUsingEncoding:NSUTF16StringEncoding];
- // UTF16, 2-byte per unit
- *length = data.length / 2;
- return data.bytes;
- }
Dart 拿到 uint16_t 数组后会转为 Uint16List 类型,并用它初始化一个 String 对象。
- Pointer<Uint64> length = allocate<Uint64>();
- Pointer<Void> result = convertNSStringToUTF16(ptr, length);
- Uint16List list = result.cast<Uint16>().asTypedList(length.value);
- free(length);
- String str = String.fromCharCodes(list);
后记
写了这么多 DartNative 的相关文章,终于轮到了介绍最基础最核心的同步调用功能。其实异步调用也是支持的,看来用 DartNative 来替换 Flutter Channel 的理由又多了。
这篇文章主要讲的是 iOS 的同步调用实现以及性能优化,Android 也已经实现同步调用中基本类型的自动转换。