如何實(shí)現(xiàn) Flutter 同步調(diào)用 Native API
Flutter Channel 是一個(gè)異步調(diào)用通道,如果想在 Dart 側(cè)同步獲取到 Native 返回的結(jié)果,調(diào)用的時(shí)候加上 await 就可以了:
- final int result = await platform.invokeMethod('hello channel');
所以這篇文章到此為止了?
不!上面這行代碼其實(shí)是個(gè)『假同步』,因?yàn)樗槐WC了 Dart 代碼的同步執(zhí)行,而 Native 代碼與 Dart 并不在同一條線程執(zhí)行。試想下,如果你通過 Flutter Channel 打日志,但由于打日志的消息是異步傳遞到 Native 的,最后日志順序可能是錯(cuò)的。而通過日志來排查一些時(shí)序性相關(guān)的 Bug 時(shí),日志的順序很重要。
因?yàn)?Flutter Channel 設(shè)計(jì)之初就是異步的,使用 await 來回切換線程所帶來的開銷不小。而且協(xié)程的 await 語法具有傳遞性,上層調(diào)用方也需要使用 await,層層傳遞。
而 DartNative (https://github.com/dart-native/dart_native) 設(shè)計(jì)之初就是同步調(diào)用的,且也支持異步調(diào)用:
- // new DNTest instance and call hello method.
- DNTest().hello('DartNative');
Why DartNative?
DartNative 是『真同步』,保證了執(zhí)行順序。同時(shí)也支持異步調(diào)用。
一行代碼實(shí)現(xiàn)同步調(diào)用,告別 Flutter Channel 膠水代碼帶來的開發(fā)成本。
同步調(diào)用性能是 Flutter Channel 的數(shù)倍。分別使用 Flutter Channel 和 DartNative 調(diào)用 fooNSString: 方法,耗時(shí)相差三到四倍。性能數(shù)據(jù)可能在不同場景下有波動,可以通過執(zhí)行 Benchmark 代碼 來對比結(jié)果。
實(shí)現(xiàn)原理
下圖以 Dart 同步調(diào)用 iOS Objective-C API 為例,描述了 DartNative 同步調(diào)用的原理。以一個(gè)字符串參數(shù)為例,講述了從 Dart String 自動轉(zhuǎn)為 Objective-C NSString 并傳遞給 hello: 方法的過程。返回值也是自動轉(zhuǎn)換類型的,由于篇幅原因沒在圖片中描述。
在實(shí)現(xiàn)了基本的同步調(diào)用后,開發(fā)重點(diǎn)也轉(zhuǎn)向了性能優(yōu)化。
方法簽名的優(yōu)化
在 Dart 同步調(diào)用 Native 時(shí),為了實(shí)現(xiàn)跨語言調(diào)用時(shí)參數(shù)和返回值類型的自動轉(zhuǎn)換,需要先獲取到 Native 的方法簽名。這里做了兩方面的性能優(yōu)化:
- 通過 DartFFI 調(diào)用 OC Runtime 獲取方法簽名占據(jù)了一定耗時(shí)??梢栽?Dart 側(cè)加一層 Cache 來減少通信和反射次數(shù)。
- 方法簽名字符串的構(gòu)成是 “TypeEncoding+offset” 的組合,跨語言之間傳遞字符串的編解碼的耗時(shí)較多,而只有 TypeEncoding 那部分才是類型自動轉(zhuǎn)換所需要的。絕大部分類型對應(yīng)的 TypeEncoding 都是固定的,于是只需要傳遞 TypeEncoding 的指針即可。
字符串轉(zhuǎn)換的優(yōu)化
Dart String 在與 Objective-C NSString 相互轉(zhuǎn)換的過程中,數(shù)據(jù)傳輸?shù)母袷降倪x擇至關(guān)重要。因?yàn)?Dart String 是使用 UTF16 編碼的,所以 DartNative 使用 Uint16List 作為數(shù)據(jù)傳輸?shù)母袷?。通過性能測試,使用 UTF16 來回傳輸字符串的總耗時(shí)(包含 Native 方法自身耗時(shí))相比 UTF8 減少了 35% 左右,如果只計(jì)算通道自動類型轉(zhuǎn)換耗時(shí)減少的比例會更多。
轉(zhuǎn)換 Dart String 為 Objective-C NSString:
使用 DartFFI 在堆上創(chuàng)建 uint16_t 數(shù)組,將 Dart String 轉(zhuǎn)為 UTF16 格式后裝載進(jìn)去。最終通過 perform 方法反射調(diào)用 stringWithCharacters:length: 方法來創(chuàng)建 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);
轉(zhuǎn)換 Objective-C NSString 為 Dart String:
NSString 轉(zhuǎn)為 UTF16 稍微麻煩一點(diǎn)。這里的方案是先轉(zhuǎn)為 UTF16 的 NSData,然后將 uint16_t 數(shù)組的地址和字符長度(不是字節(jié)長度)返回給 Dart 側(cè)。
- 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 數(shù)組后會轉(zhuǎn)為 Uint16List 類型,并用它初始化一個(gè) 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 的相關(guān)文章,終于輪到了介紹最基礎(chǔ)最核心的同步調(diào)用功能。其實(shí)異步調(diào)用也是支持的,看來用 DartNative 來替換 Flutter Channel 的理由又多了。
這篇文章主要講的是 iOS 的同步調(diào)用實(shí)現(xiàn)以及性能優(yōu)化,Android 也已經(jīng)實(shí)現(xiàn)同步調(diào)用中基本類型的自動轉(zhuǎn)換。