阅读视图

发现新文章,点击刷新页面。

Flutter 官方终于出手了,DartNative 将何去何从?

2022 年 8 月底,Flutter 发布了 3.3 稳定版,随之发布的 Dart 2.18 宣布支持 Dart 与 Objective-C 和 Swift 互调,而 Java 与 Java/Kotlin 的互调也在开发中。整体思路跟 DartNative 三年前的思路类似,走的也是跨语言 API 直接调用(但官方目前只支持同步),然后通过工具链生成接口绑定。发布当天就有人给我提 Issue 了:老哥,考虑一下这个库未来何去何从吧,官方有了,竟如此『不讲武德』

联想到之前 5 月份 Flutter 3.0(Dart 2.17) 发布时官方支持了 Dart Finalizer,跟 DartNative 一年前就支持的 Finalizer 冲突了,看来是『有备而来』啊:

ffigen 与 DartNative

Flutter 官方为了支持 Dart 与 Objective-C 和 Swift 互调,基于 ffigen 工具生成了大量的模板代码,缺点是可读性差,优点是性能好一些;DartNative 基于 Native Runtime 动态调用任意 Objective-C 和 Swift 方法,codegen 只是锦上添花,缺点是首次调用性能有所牺牲(有 cache),优点是动态性强且生成代码可读性高,即便手写代码也很少。

目前二者的实现细节差异也蛮大,比如官方的代码生成是基于 clang 的,比 DartNative codegen 基于的 antlr 更严谨一些,但是使用成本也高很多。官方的 ffigen 目前虽然从 Sample 示例上虽然还没看到对异步调用和回调等能力的支持,不过从整体上官方投入力度还是蛮大的,比我这种利用空闲时间断断续续搞的 sideproject 强多了,后续的能力补齐只是时间问题。

这三年来我曾一直怀疑 DartNative 的设计路线是否正确,现在官方亲自下场了,那说明这个思路还是有前瞻性的。不过在此之前我也一直在反思这个设计的缺点:把抹平各语言 API 的工作交给了 dart 这一层,需要写平台判断的代码,这与主流的 Channel 接口绑定方案使用上差异很大,增加了理解成本和门槛。为了补齐这块短板,我和另一位贡献者 hui19 参考了 JSI 等跨语言 bridge 接口绑定的设计,提出了开发 DartNative Interface(简称 DNI?),并在 2022 年 5 月份 DartNative 的 0.7.x 版本开始支持(比官方 8 月份发布 Dart 2.18 还要早哈哈)。

于是有意思的事情出现了:Channel/ffigen 和 DartNative 正在朝着对方的设计思路演进,但实现上却有很大差别。这里没有吹嘘 Flutter 官方是参考了 DartNative 的意思,毕竟这种只有不到 900 Star 的小项目根本不会受到 Google 官方关注,我相信这只是巧合罢了。而且 ffigen 和 DartNative 都是基于官方的 dart:ffi 实现的,所以 Google 永远都是爸爸。

什么是 DartNative Interface

DartNative Interface 实现了跨语言接口之间的绑定和双向调用。相比于 Channel,无需针对 method 写一堆 if-else,也不用把参数挤在一坨序列化和反序列化。DartNative Interface 会将参数列表和返回值自动转换,并支持同步调用和异步协程(这 Channel 它能比吗?它不可以)。iOS/macOS/Android 目前支持的数据类型:num/String/List/Map/Set/NativeByte/NativeObject,支持双向互相调用。iOS/macOS 额外支持 Function/Pointer,支持 Swift。

相比于 DartNative 之前的设计,接口绑定意味着减少了对 native 调用的动态性,但也提供了抹平多端接口差异的标准化方案。继承了 DartNative 强大的类型转换和生命周期管理能力,更加易用。

使用示例

Dart 调用 Native

Dart 代码:

1
2
3
4
5
6
7
8
9
final interface = Interface("MyFirstInterface");
// Example for string type.
String helloWorld() {
return interface.invokeMethodSync('hello', args: ['world']);
}
// Example for num type.
Future<int> sum(int a, int b) {
return interface.invokeMethod('sum', args: [a, b]);
}

对应的 Objective-C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation DNInterfaceDemo

// Register interface name.
InterfaceEntry(MyFirstInterface)

// Register method "hello".
InterfaceMethod(hello, myHello:(NSString *)str) {
return [NSString stringWithFormat:@"hello %@!", str];
}

// Register method "sum".
InterfaceMethod(sum, addA:(int32_t)a withB:(int32_t)b) {
return @(a + b);
}

@end

对应的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// load libdart_native.so
DartNativePlugin.loadSo();

@InterfaceEntry(name = "MyFirstInterface")
public class InterfaceDemo extends DartNativeInterface {

@InterfaceMethod(name = "hello")
public String hello(String str) {
return "hello " + str;
}

@InterfaceMethod(name = "sum")
public int sum(int a, int b) {
return a + b;
}
}

Native 调用 Dart

Dart 代码:

1
2
3
4
interface.setMethodCallHandler('totalCost',
(double unitCost, int count, List list) async {
return {'totalCost: ${unitCost * count}': list};
});

对应的 Objective-C 代码:

1
2
3
4
5
[self invokeMethod:@"totalCost"
         arguments:@[@0.123456789, @10, @[@"testArray"]]
            result:^(id _Nullable result, NSError * _Nullable error) {
    NSLog(@"%@", result);
}];

对应的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
invokeMethod("totalCost", new Object[]{0.123456789, 10, Arrays.asList("hello", "world")},
new DartNativeResult() {
@Override
public void onResult(@Nullable Object result) {
Map retMap = (Map) result;
// do something
}

@Override
public void error(@Nullable String errorMessage) {
// do something
}
}
);

Dart Finalizer

Flutter 3.0(Dart 2.17) 开始支持 Dart Finalizer,但是使用 DartNative,只需要 Flutter 2.2(Dart 2.13) 就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final foo = Bar(); // A custom instance.
unitTest.addFinalizer(() { // register a finalizer callback.
print('The instance of \'foo\' has been destroyed!'); // When `foo` is destroyed by GC, this line of code will be executed.
});
```

如果想在 Native 监听一个 Dart 对象被销毁,做法是 Dart 从 Native 获取一个对象,并作为另一个想要被监听的 Dart 对象的属性:

```dart
class DartLifecycleObject {
late final dynamic finalizer;
DartLifecycleObject() {
finalizer = interface.invokeMethodSync('finalizer');
}
}

这里以 OC 语言为例,可以返回一个自定义类(DNDartFinalizer)的对象:

1
2
3
InterfaceMethod(finalizer, finalizerObject) {
return [[DNDartFinalizer alloc] init];
}

当 Dart 对象(DartLifecycleObject)被释放后,DNDartFinalizer 对象也会被释放,所以在 dealloc 方法中可以监听到:

1
2
3
4
5
@implementation DNDartFinalizer
- (void)dealloc {
NSLog(@"DartLifecycleObject dead!");
}
@end

何去何从?

DartNative 的最近一次技术分享是在 2021 年底的 GMTC 深圳站:Flutter 自研通道 DartNative 的探索与实现。不过那次分享的内容有所保留,不仅没有提到 DartNative 的 Interface 开发计划,连当时已经做了的一些新特性都没有讲,比如支持 Swift 和 macOS、适配 Dart nullsafety、支持 muti-isolates、支持 OC 方法和 block 返回 Future 给 Dart 等。原因是篇幅要精简,况且总得攒点东西留着后续分享不是嘛?

当时我还是 Flutter 专题的出品人,title 长度虽然比不过龙妈,但依然被黑惨了:

接下来的故事比较有意思,手 Q 后来下掉了 Flutter,业务就缺乏了落地验证场景(iOS App 是否使用 Flutter 并不是机密,扒一下安装包就看得出来,网上也有人统计过各大 App 使用 Flutter 的情况)。考虑到腾讯 Flutter Oteam 未来的发展需要落地到具体业务,于是我把 Oteam 负责人的职位交接给了其他大佬。DartNative 依旧在修修补补,毕竟公司还有其他的 App 在使用,GitHub 也偶尔有提 Issue。下掉 Flutter 后我又接手了小程序,之前有用 Flutter 写的业务如今运行在了小程序上。缘,妙不可言。

最后就是官方宣布 ffigen 支持直接调用 Objective-C/Swift 之后,Java/Kotlin 也在进行中。我估计再给 Flutter 官方一年时间,ffigen 应该可以补齐能力。剩下的就是 Channel 与 DartNative Interface 的差异了,我只能抽业余时间尽量缝缝补补吧,也期待官方能力不断拓展,毕竟总有那么一天的。目前 DartNative 的硬伤是不支持 Windows,如果有 Windows 大佬欢迎共建。我是肝不动了,否则博客能断更两年么(手动狗头)?

实现 Native 异步回调 Flutter

看到标题的你可能已经充满疑问:Channel 不是本来就支持 Native 调用 Flutter 的么?别着急,先往下看。DartNative 要实现的是一个用 Channel 无法做到的回调场景:

为什么说 Flutter Channel 无法做到呢,有两点:

  1. 使用 Channel 从 Native 调用 Dart 时,想获取返回值就只能通过在 Channel API 在主线程的异步回调 FlutterResult。上面的例子是在 Native 的主线程调用 Flutter 并可以同步获取到返回值,如果用 Channel 会直接导致死锁。
  2. Flutter Channel 需要写额外的胶水代码,而上面的例子简单清爽,跨语言调用无缝衔接。

PS: 考虑到性能问题,在 Native 主线程调用 Flutter 并同步等待返回值这种场景,可能会引起卡顿。实事求是地说,这可能本身是个不该考虑到的场景,但不代表 DartNative 就不去做。毕竟 DartNative 在实现 Flutter Channel 没覆盖到的场景的同时,也在尝试不断替代它。

除了 Block 的回调场景,还有 iOS 里常见的 Delegate:

1
2
3
4
5
6
7
@implementation RuntimeStub
- (void)fooDelegate:(id<SampleDelegate>)delegate {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSObject *result = [delegate callback];
});
}
@end

使用 DartNative 是这么玩的:

1
2
3
DelegateStub delegate = DelegateStub();
RuntimeSon stub = RuntimeSon();
stub.fooDelegate(delegate);

当 OC 的 [delegate callback] 被执行时,Dart 类 DelegateStub 实例的 callback() 方法会被调用:

1
2
3
4
5
6
7
8
9
10
11
class DelegateStub extends NSObject with SampleDelegate {
DelegateStub() : super(Class('DelegateStub', type(of: NSObject))) {
super.registerSampleDelegate();
}

@override
callback() {
print('callback succeed!');
return NSObject();
}
}

类似 Native 需要回调给 Dart 的场景还有很多,比如 NSNotificationdealloc 等渠道。这些回调需求无法直接用 Channel 来实现,DartNative 基于 DartVM 提供的能力实现了一套回调机制,支持阻塞和非阻塞两种方式。

下面以 Block 回调为例来讲下具体实现,在这之前先熟悉下 Dart Function 是如何变成 Block 并传递给 OC 的。这部分原理在之前的《在 Flutter 中玩转 Objective-C Block》也有讲过:

Block 能够回调到对应的 Dart Function 的前提是能建立起绑定关系,也就是上图右侧的『指针绑定』那里。这里是通过 DartFFI 提供的能力来将 Dart Function 转为函数指针 callback,更加具体的关联逻辑实现如下图所示:

绑定好 OC Block 和 Dart Function 后,重点来了:如何在 Native 调用 callback 函数指针指向的函数?要知道,这个函数肯定要在 Dart Function 传入时的线程来调用的(一般是 flutter-ui 线程),而且还不能位经由 isolate 直接切到对应线程去调用。

这里使用的 Port 相关 API,在 Dart isolate 库中可以找到。不过在 Native 侧 Flutter 框架并未暴露这些 API,但是可以在 Dart VM 源码中找到。Native 异步调用到 Flutter 的实现流程如下,具体函数实现可以查阅 DartNative 源码:

上面讲完了 Block 执行后回调 Dart。Block dealloc 后回调 Dart 并释放资源的原理跟上面大同小异,只是异步回调的时候不会阻塞等待返回值罢了。更详细流程可以看之前我写的DartNative 内存自动管理》对 Block 内存的处理。

而 Delegate 和 Notification 回调的调用原理都跟 Block 回调相同,只是在上层类型封装和参数列表上有少许差异,这里不再赘述。

如何实现 Flutter 同步调用 Native API

Flutter Channel 是一个异步调用通道,如果想在 Dart 侧同步获取到 Native 返回的结果,调用的时候加上 await 就可以了:

1
final int result = await platform.invokeMethod('hello channel');

所以这篇文章到此为止了?

不!上面这行代码其实是个『假同步』,因为它只保证了 Dart 代码的同步执行,而 Native 代码与 Dart 并不在同一条线程执行。试想下,如果你通过 Flutter Channel 打日志,但由于打日志的消息是异步传递到 Native 的,最后日志顺序可能是错的。而通过日志来排查一些时序性相关的 Bug 时,日志的顺序很重要。

因为 Flutter Channel 设计之初就是异步的,使用 await 来回切换线程所带来的开销不小。而且协程的 await 语法具有传递性,上层调用方也需要使用 await,层层传递。

DartNative 设计之初就是同步调用的,且也支持异步调用:

1
2
// new DNTest instance and call hello method.
DNTest().hello('DartNative');

Why DartNative?

  1. DartNative 是『真同步』,保证了执行顺序。同时也支持异步调用。
  2. 一行代码实现同步调用,告别 Flutter Channel 胶水代码带来的开发成本。
  3. 同步调用性能是 Flutter Channel 的数倍。分别使用 Flutter Channel 和 DartNative 调用 fooNSString: 方法,耗时相差三到四倍。性能数据可能在不同场景下有波动,可以通过执行 Benchmark 代码 来对比结果。

实现原理

下图以 Dart 同步调用 iOS Objective-C API 为例,描述了 DartNative 同步调用的原理。以一个字符串参数为例,讲述了从 Dart String 自动转为 Objective-C NSString 并传递给 hello: 方法的过程。返回值也是自动转换类型的,由于篇幅原因没在图片中描述。

在实现了基本的同步调用后,开发重点也转向了性能优化。

方法签名的优化

在 Dart 同步调用 Native 时,为了实现跨语言调用时参数和返回值类型的自动转换,需要先获取到 Native 的方法签名。这里做了两方面的性能优化:

  1. 通过 DartFFI 调用 OC Runtime 获取方法签名占据了一定耗时。可以在 Dart 侧加一层 Cache 来减少通信和反射次数。
  2. 方法签名字符串的构成是 “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 对象。

1
2
3
4
5
6
7
8
9
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 侧。

1
2
3
4
5
6
7
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 对象。

1
2
3
4
5
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 也已经实现同步调用中基本类型的自动转换。

DartNative 内存自动管理

DartNative 可以让开发者一行代码实现调用 Native 代码,且支持高性能同步调用。之前曾经写过一篇文章讲述 Dart 与 Objective-C 对象的生命周期管理,当时的实现是『半自动』的解决方案。如今 DartNative 更新到 0.3 后实现了生命周期的自动管理,也就是『全自动』的解决方案。

新版本的变化

DartNative 0.3 版本基于 Flutter 1.20.2,Dart 1.9.0。我不得不提的是,Flutter 和 Dart 对 API 兼容性设计的确很糟糕,不仅没有 API Available 的文档或语法标注,也经常会发生 breaking change。DartNative 也因此表现的同样『激进』,毕竟我们还没有发布 1.0 版本。

新版本不再需要手动管理内存,Objective-C 对象会被对应的 Dart 对象持有,当 Dart 对象析构时就不再持有 Objective-C 对象。于是 iOS 侧直接干掉了 NSObject 和各种 structretain()release() 等手动操作引用计数的方法。如果从旧版本升级过来,发现编译失败,直接删掉对这些方法的调用就可以。

实现原理

这里只讲下对象的生命周期管理,非对象类型基本上依然复用之前的策略

Dart 封装的 NSObject 对象

之前我写过一篇文章《DartNative Memory Management: NSObject》,其中讲述了对象类型的内存管理,并且埋了个坑:

如果 Dart VM 支持了 finalize,那么现在的『半自动』内存管理就成了『全自动』了,不过那样的话,内存管理方案也会改变。

嗯,下图简要描述如何手动实现 Dart Finalizer,并将 Objective-C 对象的生命周期『部分绑定』到 Dart 对象上的。这里之所以是『部分绑定』,是考虑到 Objective-C 对象不仅可以被 Dart 对象持有,也可以被其他 Objective-C 对象持有。Dart 对象的构造和析构只是对关联的 Objective-C 对象引用计数加一和减一:

Flutter 内嵌的 Dart VM 中也内嵌了一些 C 的 API 来供 Native Extension 调用。虽然可以在二进制文件中链接到函数符号,但开发时依然需要引入 Dart SDK 源码中相应的头文件来进行编译。

由 Dart Function 创建的 Block 对象

还有一种特殊场景,就是 Dart 调用带有 Block 回调的 API。此时是 Native 的 Block 来回调 Dart 函数,需要保证回调的时候 Dart 函数及上下文依然存在。此时的 Dart Function 会被 Dart Block 持有,而 Dart Block 的生命周期被绑定到了对应的 Objective-C Block 上:

Objective-C 的 Block 析构时,会通过 isolate 注册好的 Port 去异步回调 Dart。实际的实现其实比上图描述的还要复杂得多,这里为了阐述思路,简化了很多细节。其实像 dealloc 这种无需阻塞的异步回调可以直接用 Flutter Channel 来完成,但因为 DartNative 基于 Dart VM API 和 FFI 等技术搭建了通用的异步回调机制,这里直接复用这个能力了。至此 DartNative 彻底告别了 Flutter Channel。

结论

DartNative 0.3 版本虽然没有新的 Feature,但其实是在修内功。在 iOS 侧的内存管理和异步回调上修复了一系列问题,后续还会将对应能力同步到 Android 侧。

Reference

如何实现一行命令自动生成 Flutter 插件

在上一篇文章《告别 Flutter Channel,调用 Native API 仅需一行代码!》 发出后,收到了很多关注。仔细想想,其实不是仅仅只需一行代码的,还需要敲一行 codegen 命令来生成 Dart 代码。这回就简单讲下自动生成代码这块的设计和实现原理。

为何要做代码生成工具

一开始技术架构的搭建自底向上的。当我做出 Flutter 与 Native 之间的高性能通道后,自然而然地去想要提升易用性,降低开发者的使用门槛。当你从使用者角度去审视自己的产品时,就会自顶向下去设计一些 Feature 去满足目标用户的诉求。

最终我决定开发一款命令行工具。它可以解析 Native 代码中的 API,生成对应的 Dart 代码,再进而支持生成 Flutter Plugin/Package 工程。生成的 Flutter 工程会通过 pub 依赖 DartNative:

工具需要满足以下需求:

  1. 兼容性:能够支持 Native 多种语言代码的转换,如 Objective-C,Java,Swift,Kotlin 等
  2. 易用性:开发者能够很方便地安装和使用工具

技术方案

为了满足兼容性,在实现『解析代码生成 AST』的技术方案上,是基于第三方开源框架 antlr 来实现。命令行工具使用 NodeJS 实现,开发者可以通过 npm 很方便地安装工具。

如果基于 Clang 来实现的话,性能和稳定性都有保证,但是需要 link 到所需的代码或 framework。且 Clang 对多语言的支持比较局限,也不方便提供给开发者去安装。

关于 antlr 的使用,这里不再赘述,可以直接查看官方文档。(毕竟作者是靠卖书赚钱的)。这里需要将源语言的 grammer 生成 JS 版本的 Runtime 文件,在遍历 AST 的 callback 中收集所需的元数据,转换成为生成 Dart 自定义格式的 AST。最后遍历 Dart AST,生成 Dart 代码。

这里的方案概括为如下步骤:

  1. 查找出包含客户端 API 的 Native 代码文件,如 .h.java 文件
  2. 通过 antlr parser 生成 Native 语言的 AST
  3. 将 Native AST 转换成 Dart 语言所需的 AST
  4. 通过 Dart AST 生成 dart 代码

遇到的坑

词法分析失败

官方提供的 Objective-C 的 grammer 有很多问题,生成的 lexer 还是 parser 都会在词法分析阶段就抛异常。这里就比较坑了,需要修改 lexer 和 parser 的 g4 文件。可能是 grammer 太久没更新了,很多分词阶段就抛异常了,比如 @import 都不支持,真是一言难尽。而且这种只针对单个文件的词法分析程序,很难像 Clang 那样可以 link 其他文件做到的严谨性。经过一系列的修复工作,已经可以 parse iOS Foundation 库的所有头文件。

语法特性映射

这一步发生在 AST 的 transform。虽说不同语言之间有很多语法设计都是想通的,但是依然会有一些难以映射的语法特性。比如 Java 的重载方法,OC 奇葩的方法名。转换成 Dart 的方法命名应该遵循哪方的语言规范?OC 的 Protocol 在 Dart 中如何表示?Dart 类的静态方法不会被继承,那么 Protocol 中的类方法怎么办?Dart 的 enum 不支持自定义 int 值,来自 OC 的 enum 如何转换成 Dart?区分何时生成 Dart 的 importexport?。。。

类似这些操蛋的问题数不胜数。。。需要不断细化,思考,打磨。。。

批量处理

当要处理的文件有很多时,一个一个地串行处理显然会让开发者等得不耐烦。NodeJS 本来不是为 CPU 密集型操作服务的,但是在 v10.5.0 引入 worker_threads 后,实现了真·多线程,解决了 CPU 密集型操作的痛点。

在处理一些稍大点的文件时,需要注意上调 NodeJS VM 老生代内存的阈值。

多种使用场景

考虑到不同的使用场景,codegen 所生成的策略也会不一样:

  1. 将 App 的 Native 代码转为 Dart 代码,直接在 Flutter 中使用
  2. 将 Native 系统库转成 Flutter Package
  3. 将 Native 第三方库转成 Flutter Plugin

这三种场景的共同点:如果 Native 代码依赖了其他库,也需要向 pubspec.yaml 中插入这个依赖库的 Dart 版本。

第 2 和 3 个场景需要用 flutter create 命令生成新的 Flutter 工程,并将生成的 Dart 代码挪到工程里。
更进一步是将生成代码所需的 Native 文件也挪到 Flutter 工程里,并更新 podspec 和 gradle (PS:待实现)。

总结

在做 codegen 之前,我一直觉得有 dart_native 超级通道就足够了。毕竟是自己开发的轮子自己熟悉,认为手写一层 Dart Wrapper 也没啥麻烦的,其实并不然。当给未接触过的开发者使用后,的确是有上手门槛和开发量的。在调研的过程中也发现 bang 神的 JSPatch 也有个 Converter 工具用来把 OC 代码转为 JS 代码,同样用的也是 antlr 解析 AST,在此也十分感谢 JSPatch 提供的思路。超级通道加工具辅助方可实现了运行性能和开发效率的双提升

Reference

antlr
grammers-v4
JSPatch Convertor 实现原理详解

告别 Flutter Channel,调用 Native API 仅需一行代码!

DartNative 自研超级通道的性能已经数倍优于 Flutter Channel 之后,我将目光转向了开发成本的优化。于是 Codegen 应运而生,开发者可以用它很方便地将 Native API 转为 Dart 封装,直接拿来用就可以了!从而优化 Flutter 调用 Native API 的开发体验,实现『运行性能和开发效率的双提升』:

  • 无需编写 Flutter Channel 的胶水代码
  • 无需跨 IDE 联调 Channel 两边的代码
  • Native API 也被赋予了热重载功能
  • 支持同步调用,打日志顺序不再错乱

如果你还一脸懵逼,来看一段实操视频吧(第一次做 UP 主,跪求一键三连):

视频中为了演示方便,对 Codegen 代码有些特殊处理:去掉了自动生成 import 的代码。

DartNative 整体解决方案&展望

  1. DartNative Bridge: 自研超级通道,性能甩开官方 Flutter Channel 好几倍,支持 Native 绝大部分类型
  2. DartNative Codegen: 将 Native API 转为 Dart API,可在 Flutter 工程中直接调用
  3. DartNative Dispatch: 各平台 API 终究是有差异的,且只能靠开发者手动抹平。通过分发机制为开发者提供一个抹平平台差异代码的地方。
  4. DartNative Component Market:基于 DartNative 的开源组件市场,未来会有越来越多的 Native 组件通过 DartNative 转为 Flutter 组件。

从 2019 年的九月份开始做 DartNative 的第一个版本(那时候还叫 DartObjC),到如今初具规模并在线上小范围使用,可谓是有些漫长。漫长的原因有两点:

  1. 为了追求性能与效率双提升,技术方案上走了 Hard 模式。抛弃 Channel 是一条没人走过的路,虽说 DartNative 开源后陆陆续续出现了一些相同 idea 的项目,但都没有走我这条最艰难的路。不仅技术有难度,设计方案也要反复推翻,打磨,优化。。。做新的解决方案就是很漫长,我最然做的很早,但是战线拉得太长。
  2. 这是一个利用打游戏剩下的业余时间搞出来的 side project,全凭自身兴趣和满腔热血。有时候也羡慕那些有 KPI 的开源项目,起码有排期的保证,能够快速推进项目进度。

个人的力量终究是有限的,尤其是 Android 我一窍不通。还好后来也有更多感兴趣的小伙伴加入这个项目,补齐 Android 侧的超级通道能力,继续推进 Codegen 的完成度。

Codegen 的用法

Codegen 是一个 CLI 工具,可以很方便地使用 npm 来安装:

1
$ npm install -g @dartnative/codegen

跟其他标准的 CLI 工具一样,通过 -h 选项可以查看用法:

1
2
3
4
5
6
7
8
9
10
Usage: codegen [options] <input>

Generate dart code from native API.

Options:
-V, --version output the version number
-l, --language <language> [objc, java, auto(default)]
-o, --output <output> Output directory
-p, --package <package> Generate a shareable Flutter project containing modular Dart code.
-h, --help display help for command

Codegen 默认会自动监测输入源代码的语言,目前还只支持 Objective-C 语言。默认的 Dart 文件输出目录是当前目录,也可以通过 -o 选项来指定输出目录。生成的 Dart 代码会通过 DartNative 的超级通道(Bridge)来调用 Native API。

Codegen 还支持将一个 Native SDK 转成 Flutter 组件,不过此功能尚在完善中,也就是 -p 选项。

后记

曾经有两位大佬看了 DartNative 后问我有没有计划将它 Merge 到 Flutter 官方仓库里,我那时候觉得这个想法真的很大胆。现在看起来,如果完成了上述解决方案的大部分,好像也并不是不可以。

Passing Out Parameter in DartNative

dart_native 作为一条比 Channel 性能更高开发成本更低的超级通道,通过 C++ 调用 Native 的 API,深入底层且考虑全面。很多 Objective-C 接口含有 NSError ** 这种 out parameter,dart_native 也对这种场景做了支持。

封装 Objective-C 里的 Out Parameter

说白了用的最多的就是 “A pointer to a pointer” 啊!NSError ** 啊!

1
2
NSError *error;
[self fooWithError:&error];

那换成 Dart 语言该咋表示呢???首先要知道 Dart 是不支持 out parameter 的,只能另辟蹊径,在语法上做一些妥协,最终跑通流程实现目的。

1
2
NSObjectRef<NSObject> ref = NSObjectRef<NSObject>();
fooWithError(ref);

还记得之前 dart_native 是如何封装 NSObject * 的么?用一个同名的 Dart 类包一个 OC 对象的指针就行了。那想封装 out parameter 的话,在此基础之上再套一层不就行了!只要用泛型,就能一层层套下去。。。

1
2
3
4
class NSObjectRef<T extends id> {
T value;
Pointer<Pointer<Void>> _ptr;
}

接着要考虑如何初始化 out parameter 了。在 OC 里只需要在栈上的一个地址就够了,也就是声明一个变量。但 Dart 的对象并没有对应指针的概念,但是可以通过 dart ffi 手动创建一个指向指针的指针。不过它指向的内存是在堆上,需要手动释放。此时可以通过我之前讲内存管理的文章里讲到的 PointerWrapper 来实现临时指针变量的自动释放,简单来说就是把 dart ffi 创建的内存交给 OC ARC 管理。

加上构造方法和自动释放后的 NSObjectRef 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class NSObjectRef<T extends id> {
T value;
Pointer<Pointer<Void>> _ptr;
Pointer<Pointer<Void>> get pointer => _ptr;

NSObjectRef() {
_ptr = allocate<Pointer<Void>>();
_ptr.value = nullptr;
PointerWrapper wrapper = PointerWrapper(_dealloc);
wrapper.value = _ptr.cast<Void>();
}

NSObjectRef.fromPointer(this._ptr);

_dealloc() {
_ptr = null;
}
}

从 Out Parameter 取值

Dart 侧把一个指针传给 OC 后,OC 会创建另一个指针,并把后者赋值给前者指向的内存。还是拿 NSError 举例子:

1
2
3
4
5
- (void)fooWithError:(out NSError **)error {
if (error) {
*error = [NSError errorWithDomain:@"com.dartnative.test" code:-1 userInfo:nil];
}
}

下一步是要将上例中 OC 的 NSError 对象转成 Dart 的对象,并赋值给 NSObjectRefvalue 属性上。

建立泛型与初始化的映射

面对不同泛型的 NSObjectRef 声明,要转成其封装类型的对象。而 Flutter 禁用的 Dart 的反射,即不能通过 NSObjectRef 声明的泛型来初始化对应的类。我维护了个 Map 来建立起 Type 到初始化调用的映射,并提供注册方法:

1
2
3
4
5
6
7
8
9
typedef dynamic ConvertorFromPointer(Pointer<Void> ptr);

Map<String, ConvertorFromPointer> _convertorCache = {};

void registerTypeConvertor(String type, ConvertorFromPointer convertor) {
if (_convertorCache[type] == null) {
_convertorCache[type] = convertor;
}
}

这样调用 registerTypeConvertor 函数就可以很方便地建立起 Native 封装类型到初始化闭包的映射:

1
2
3
registerTypeConvertor('NSString', (ptr) {
return NSString.fromPointer(ptr);
});

接着实现 convertFromPointer 函数,用来调用之前注册的闭包,这样就实现用类名和指针来获取到对应的 Dart 对象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dynamic convertFromPointer(String type, dynamic arg) {
Pointer<Void> ptr;
if (arg is NSObject) {
ptr = arg.pointer;
} else if (arg is Pointer) {
ptr = arg;
} else {
return arg;
}

if (ptr == nullptr) {
return arg;
}

ConvertorFromPointer convertor = _convertorCache[type];
if (convertor != null) {
return convertor(ptr);
} else if (arg is Pointer) {
return NSObject.fromPointer(arg);
}
return arg;
}

最后在 NSObjectRef 里添加了个 syncValue 方法,将转换好的 Dart 对象赋值给 value 属性:

1
2
3
4
5
syncValue() {
if (_ptr != null && _ptr.value != nullptr) {
value = convertFromPointer(T.toString(), _ptr.value);
}
}

自动生成注册代码

那么多 Native 类型,总不能手写代码一个个去调用 registerTypeConvertor 吧。dart_native 提供了 Annotation 用于自动生成这些注册代码,只需要在封装 Native 类的上面加一个 @native 即可:

1
2
3
4
5
6
@native
class NSString extends NSSubclass<String> {
NSString.fromPointer(Pointer<Void> ptr) : super.fromPointer(ptr) {
value = perform(SEL('UTF8String'));
}
}

这样只需要在项目目录里运行下面的命令,所有加了 @native 的类都会在同一个 dart 文件中生成注册初始化闭包的代码:

1
flutter packages pub run build_runner build --delete-conflicting-outputs

建议在运行上面的 build 之前先 clean 下:

1
flutter packages pub run build_runner clean

这是 dart_native 里带的一份自动生成的文件 ``:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// DartNativeGenerator
// **************************************************************************

import 'package:dart_native/dart_native.dart';
import 'package:dart_native/src/ios/foundation/collection/nsarray.dart';
import 'package:dart_native/src/ios/foundation/collection/nsdictionary.dart';
import 'package:dart_native/src/ios/foundation/collection/nsset.dart';
import 'package:dart_native/src/ios/foundation/nsvalue.dart';
import 'package:dart_native/src/ios/foundation/nsnumber.dart';
import 'package:dart_native/src/ios/foundation/notification.dart';
import 'package:dart_native/src/ios/foundation/nsstring.dart';

void runDartNative() {
registerTypeConvertor('NSArray', (ptr) {
return NSArray.fromPointer(ptr);
});

registerTypeConvertor('NSDictionary', (ptr) {
return NSDictionary.fromPointer(ptr);
});

registerTypeConvertor('NSSet', (ptr) {
return NSSet.fromPointer(ptr);
});

registerTypeConvertor('NSValue', (ptr) {
return NSValue.fromPointer(ptr);
});

registerTypeConvertor('NSNumber', (ptr) {
return NSNumber.fromPointer(ptr);
});

registerTypeConvertor('NSNotification', (ptr) {
return NSNotification.fromPointer(ptr);
});

registerTypeConvertor('NSString', (ptr) {
return NSString.fromPointer(ptr);
});
}

考虑到 Flutter 的 plugin 和 App 都可能会用到 dart_native,那么各自的 Native 类就都要生成对应的注册代码。所以这里的入口函数名是根据 package 名生成的,不用担心重名问题。

利用 Annotation 自动生成代码的实现原理就不细说了,网上文章很多,可以参考闲鱼的 annotation_route。我只是做了一点微小的优化工作,可能以后也不会单开一片文章来讲。

PS: 自动生成代码这块一开始是给 callback 功能用的,这里写下,只是蹭了蹭篇幅。

自动取值

syncValue() 方法实现后就比较简单了,下一步就只是找个合理的时机调用的问题了。这只需要在 dart_nativemsgSend 方法中加入对参数类型的判断。如果是 NSObjectRef 类型,则需要在调用完 Native 侧的方法后再次调用它的 syncValue() 方法。

这里仅截取一段相关的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 省略部分逻辑
List<NSObjectRef> outRefArgs = [];
// 省略部分逻辑
if (args != null) {
// 省略部分逻辑
for (var i = 0; i < argCount; i++) {
var arg = args[i];
if (arg == null) {
arg = nil;
} else if (arg is NSObjectRef) {
outRefArgs.add(arg);
}
// 省略部分逻辑
}
}
outRefArgs.forEach((ref) => ref.syncValue());

dart_native 中的msgSend 方法顾名思义,虽然表面上是复刻 OC 的实现,实则接口和原理差很多。这里也不详细展开讲,感兴趣的可以直接去看代码。

后续

NSObjectRef 目前只考虑了对 NSObject 及其子类的 out parameter 的封装,理论上对其他基本类型和结构体也是可以支持的,不过使用场景可能没 NSError ** 那么多,等遇到的时候再搞吧。

内行看门道,外行看热闹。我这么简单的内容都能水出一篇文章,跪求大佬们轻喷,不嘲笑就好。

在 Flutter 中玩转 Objective-C Block

dart_native 作为一条比 Channel 性能更高开发成本更低的超级通道,通过 C++ 调用 Native 的 API,深入底层且考虑全面。很多 Objective-C 接口的参数和返回值是 Block,所以这就需要支持用 Dart 语言创建和调用 Objective-C Block。

Dart 调用 Objective-C 带 Block 的 API

Dart 语言支持协程,这样就无需传递闭包来作为异步调用的回调。而 Objective-C 大量 API 都使用 Block 作为回调,当 Dart 调用这类异步 API 的时候,就需要 Dart 侧创建 Block 并传递给 Objective-C。

Dart 语言中的 Function 可以当做闭包,可以实现下面这样的效果:

1
2
3
4
stub.fooBlock((NSObject a) {
print('hello block! ${a.toString()}');
return a;
});

而对应的 Objective-C 接口如下:

1
2
typedef NSObject *(^BarBlock)(NSObject *a);
- (void)fooBlock:(BarBlock)block;

下面就讲下 dart_native 是如何做到把 Dart Function 当做 Block 传给 Objective-C 的。

函数签名

首先要确保的是 Dart Function 的签名跟 Objective-C Block 是一致的,这样二者才能转换。在 Dart 里一切皆为对象,Function 也不例外。那么拿到 Function 的 runtimeType 即可,然后解析其内容。不过 runtimeType 的内容都是 Dart 类名,如何能与 Objective-C 类型对应上呢?dart_native 的策略是提供与 Native 同名的类,这样使用这些同名类定义 Dart Function,就可以把函数签名映射到 Native 上了。

列举一些 Dart 声明的基础类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class unsigned_char = char with _ToAlias;
class short = NativeBox<int> with _ToAlias;
class unsigned_short = NativeBox<int> with _ToAlias;
class unsigned_int = NativeBox<int> with _ToAlias;
class long = NativeBox<int> with _ToAlias;
class unsigned_long = NativeBox<int> with _ToAlias;
class long_long = NativeBox<int> with _ToAlias;
class unsigned_long_long = NativeBox<int> with _ToAlias;
class size_t = NativeBox<int> with _ToAlias;
class NSInteger = NativeBox<int> with _ToAlias;
class NSUInteger = NativeBox<int> with _ToAlias;
class float = NativeBox<double> with _ToAlias;
class CGFloat = NativeBox<double> with _ToAlias;
class CString = NativeBox<String> with _ToAlias;

动态创建 Block

有了函数签名,如何构造对应的 Block 对象呢?首先要知道 Block 是什么,而这是就又个老生常谈的话题了。我十分建议你先了解下 BlockHook 及其相关文章,这样会对理解这部分内容有很大帮助。

废话不多说,上硬核:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (void)initBlock {
const char *typeString = self.typeString.UTF8String;
int32_t flags = (BLOCK_HAS_COPY_DISPOSE | BLOCK_HAS_SIGNATURE);
// Struct return value on x86(32&64) MUST be put into pointer.(On heap)
if (typeString[0] == '{' && (TARGET_CPU_X86 || TARGET_CPU_X86_64)) {
flags |= BLOCK_HAS_STRET;
}
// Check block encoding types valid.
NSUInteger numberOfArguments = [self _prepCIF:&_cif withEncodeString:typeString flags:flags];
if (numberOfArguments == -1) { // Unknown encode.
return;
}
self.numberOfArguments = numberOfArguments;
if (self.hasStret) {
self.numberOfArguments--;
}

_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&_blockIMP);

ffi_status status = ffi_prep_closure_loc(_closure, &_cif, DNFFIBlockClosureFunc, (__bridge void *)(self), _blockIMP);
if (status != FFI_OK) {
NSLog(@"ffi_prep_closure returned %d", (int)status);
abort();
}

struct _DNBlockDescriptor descriptor = {
0,
sizeof(struct _DNBlock),
(void (*)(void *dst, const void *src))copy_helper,
(void (*)(const void *src))dispose_helper,
typeString
};

_descriptor = malloc(sizeof(struct _DNBlockDescriptor));
memcpy(_descriptor, &descriptor, sizeof(struct _DNBlockDescriptor));

struct _DNBlock simulateBlock = {
&_NSConcreteStackBlock,
flags,
0,
_blockIMP,
_descriptor,
(__bridge void*)self
};
_signature = [NSMethodSignature signatureWithObjCTypes:typeString];
_block = (__bridge id)Block_copy(&simulateBlock);
SEL selector = NSSelectorFromString(@"autorelease");
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
_block = [_block performSelector:selector];
#pragma clang diagnostic pop
}

简单来说,动态创建 Block 的流程封装在了一个 Wrapper 类中,步骤如下:

  1. 用 libffi 动态创建相同签名的函数,
  2. 准备好创建 Block 需要的 flagdescriptionsignaturewrapper 对象等
  3. 根据 Block 的内存模型创建对应的结构体(栈上)
  4. 把 Block 对象 copy 到堆上,并发送 autorelease 消息

这上面每一步其实都不简单,单独拆出来都能写一段。但因为 bang 大佬已经写过文章介绍过了,我这里就不再赘述了。我只是站在巨人的肩膀上,增加了一些改进和对 Dart 的适配(如支持结构体、x86 兼容等)。很惭愧,就做了一点微小的工作。

映射 Block 和 Dart Function

Block 对象创建好了,需要跟 Dart Function 映射起来,然后当 Block 被执行的时候才会调用到对应的 Dart 逻辑。

关于回调这块,我在 Dart 侧维护一个 Map 来管理 Native 到 Dart 的回调映射。基本思路是,Key 为 Native 对象的地址,Value 为 Dart 侧的 Block 类。

Dart 版的 Block 类构造方法里会将映射建立起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
factory Block(Function function) {
List<String> dartTypes = _dartTypeStringForFunction(function);
List<String> nativeTypes = _nativeTypeStringForDart(dartTypes);
Pointer<Utf8> typeStringPtr = Utf8.toUtf8(nativeTypes.join(', '));
NSObject blockWrapper =
NSObject.fromPointer(blockCreate(typeStringPtr, _callbackPtr));
int blockAddr = blockWrapper.perform(SEL('blockAddress'));
Block result = Block._internal(Pointer.fromAddress(blockAddr));
free(typeStringPtr);
result.types = dartTypes;
result._wrapper = blockWrapper;
result.function = function;
_blockForAddress[result.pointer.address] = result;
return result;
}

Block 类的 dealloc 方法里会移除映射,防止造成 Dart 版的『野指针』。

1
2
3
4
5
dealloc() {
_wrapper = null;
_blockForAddress.remove(pointer.address);
super.dealloc();
}

Dart 调用 Objective-C 返回的 Block

结合对 Block 的理解以及实践过 Dart 调用 OC 方法的经验,很容易在 Dart 版的 Block 中实现个 invoke 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
dynamic invoke([List args]) {
if (pointer == nullptr) {
return null;
}
Pointer<Utf8> typesEncodingsPtr = _blockTypeEncodeString(pointer);
Pointer<Int32> countPtr = allocate<Int32>();
Pointer<Pointer<Utf8>> typesPtrPtr =
nativeTypesEncoding(typesEncodingsPtr, countPtr, 0);
int count = countPtr.value;
free(countPtr);
// typesPtrPtr contains return type and block itself.
if (count != (args?.length ?? 0) + 2) {
throw 'Args Count NOT match';
}

Pointer<Pointer<Void>> argsPtrPtr = nullptr.cast();
if (args != null) {
argsPtrPtr = allocate<Pointer<Void>>(count: args.length);
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (arg == null) {
arg = nil;
}
String encoding = Utf8.fromUtf8(typesPtrPtr.elementAt(i + 2).value);
storeValueToPointer(arg, argsPtrPtr.elementAt(i), encoding);
}
}
Pointer<Void> resultPtr = blockInvoke(pointer, argsPtrPtr);
if (argsPtrPtr != nullptr.cast()) {
free(argsPtrPtr);
}
String encoding = Utf8.fromUtf8(typesPtrPtr.elementAt(0).value);
dynamic result = loadValueFromPointer(resultPtr, encoding);
return result;
}
}

简单来说上面的实现做了如下几步:

  1. 获取 Block 的函数签名
  2. 校验 Dart 测传入的参数列表是否符合函数签名
  3. 将 Dart 参数转为 Native 对应的类型,写入堆中
  4. 调用 C 函数 blockInvoke,将 Block 指针和参数列表二级指针传过去
  5. 释放二级指针(其指向的对象类型和堆上的结构体会自动释放)
  6. blockInvoke 返回的指针内容转为 Dart 对象

后续

关于 Block 这块其实还有很多技术细节没有叙述完整,包括 copy 方法的实现,回调映射的细节,类型自动转换的细节等。因为篇幅原因,感兴趣的可以直接看源码:https://github.com/dart-native/dart_native

其实我期望的是使用 Dart 的协程来完成处理异步回调,这样更现代更优雅。日后会基于此方案再次封装上层接口,支持协程。

dart_native 作为一条深入底层且考虑全面的 Dart 到 Native 超级通道,未来还要做的事情还有很多。

DartNative Struct

dart_native 基于 Dart FFI,通过 C++ 调用 Native 的 API。很多 Objective-C 接口的参数和返回值都有 Struct,比如最常见的 CGSize 等。这就需要能够用 Dart 语言表示 Struct 类型,尤其是 Cocoa 内建的这些常用结构体。

结构体的存储需要一段连续的内存,可以是栈也可以是堆上。而 Dart 与 Objective-C 跨语言调用时只能传递一个指针大小的数据,这就使得 DartNative 的结构体需要在堆上创建,并通过指针传递。

Dart FFI 虽然提供了构建结构体的 API,但是目前还不支持结构体的嵌套,所以像 CGRect 包含 CGPointCGSize 这种结构,还不能通过嵌套的方式复用实现代码。此外,CGFloatNSUInteger 也可能有 32bit 和 64bit 两种情况,Dart 只能在运行时去区分该用哪种。这些原因导致目前使用 Dart FFI 构建 Struct 时不得不采用排列组合式的笨方法。

下面就以实现一个 CGSize 为例,看看这种方式有多笨。

首先 CGSize 是由两个 CGFloat 组成,而 CGFloat 又有可能是 32bit 或 64bit。所以现需要分别实现这两种情况,也就是 CGFloat32x2CGFloat64x2,分别表示两个 float 和两个 double

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CGFloat32x2 extends Struct {
@Float()
double a;
@Float()
double b;

factory CGFloat32x2(double a, double b) => allocate<CGFloat32x2>().ref
..a = a
..b = b;

factory CGFloat32x2.fromPointer(Pointer<CGFloat32x2> ptr) {
return ptr.ref;
}
}

class CGFloat64x2 extends Struct {
@Double()
double a;
@Double()
double b;

factory CGFloat64x2(double a, double b) => allocate<CGFloat64x2>().ref
..a = a
..b = b;

factory CGFloat64x2.fromPointer(Pointer<CGFloat64x2> ptr) {
return ptr.ref;
}
}

CGFloat64x2 初始化时会在堆上开辟内存,并填充数据。而使用 fromPointer 类方法初始化时则是传入一个指针,并将指针指向的内存按照内存模型映射到 Dart 这边的属性。而从 Native 那边传过来的指针肯定也是指向由 DartNative 开辟的内存,所以使用这两种初始化方法后,DartNative 都需要负责释放内存。

我也很想把这一长串代码合并下,可惜目前 Dart FFI 的语法还很弱,Dart 的类型安全编译检查也使得一些事情做不了。考虑到 Flutter 禁用了反射,所以只能按部就班写一坨一坨长得很像但又不完全一样的代码了。

基于两种情况之上再封装一层 CGFloatx2Wrapper,内部判断该用哪种。由于 Dart 不支持宏,无法在编译器静态判断是否是 64bit,所以封装了个 LP64 在运行时判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CGFloatx2Wrapper extends NativeStruct {
CGFloat32x2 _value32;
CGFloat64x2 _value64;

CGFloatx2Wrapper(double a, double b) {
if (LP64) {
_value64 = CGFloat64x2(a, b);
} else {
_value32 = CGFloat32x2(a, b);
}
wrapper;
}

Pointer get addressOf => LP64 ? _value64.addressOf : _value32.addressOf;

CGFloatx2Wrapper.fromPointer(Pointer<Void> ptr) {
if (LP64) {
_value64 = CGFloat64x2.fromPointer(ptr.cast());
} else {
_value32 = CGFloat32x2.fromPointer(ptr.cast());
}
}
}

CGFloatx2Wrapper 继承了 NativeStruct,后者内部维护了一个 PointerWrapper 来实现 Struct 堆内存的自动释放。PointerWrapper 本质上只是包装了下指针,并在自己释放的时候 free 指针指向的内存。NativeStruct 提供 retainrelease 方法,并在释放时回调 dealloc 接口,使得 Struct 在 Dart 上可以像对象类型一样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class NativeStruct {
Pointer get addressOf;

PointerWrapper _wrapper;
PointerWrapper get wrapper {
if (_wrapper == null) {
_wrapper = PointerWrapper(dealloc);
}
Pointer<Void> result = addressOf.cast<Void>();
_wrapper.value = result;
return _wrapper;
}

NativeStruct retain() {
wrapper.retain();
return this;
}

release() => wrapper.release();

dealloc() {}
}

继续回到 CGFloatx2Wrapper,这层封装内部维护两个属性 ab 及其存取方法,只是简单的透传而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
double get a => LP64 ? _value64.a : _value32.a;
set a(double a) {
if (LP64) {
_value64.a = a;
} else {
_value32.a = a;
}
}

double get b => LP64 ? _value64.b : _value32.b;
set b(double b) {
if (LP64) {
_value64.b = b;
} else {
_value32.b = b;
}
}

CGFloatx2Wrapper 也通过重写操作符实现了 Struct 判等的功能,这样就不需要使用 Objective-C 里繁琐的 CGSizeEqualToSize 等函数了:

1
2
3
4
5
6
7
bool operator ==(other) {
if (other == null) return false;
return a == other.a && b == other.b;
}

@override
int get hashCode => a.hashCode ^ b.hashCode;

真不敢相信如此难堪的代码出自我之手,只能说各位大佬们有懂 Dart 的可以指点下小弟有没有更优雅的方式。最后基于这个 CGFloatx2Wrapper 就可以封装出 CGSize,CGPoint,CGVectorUIOffset 等 Struct 了,它们均由两个 CGFloat 组成。也就只有这一步复用代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CGSize extends CGFloatx2Wrapper {
double get width => a;
set width(double width) {
a = width;
}

double get height => b;
set height(double height) {
b = height;
}

CGSize(double width, double height) : super(width, height);
CGSize.fromPointer(Pointer<Void> ptr) : super.fromPointer(ptr);
}

还有其他类型需要封装,比如 CGRect 由 4 个 CGFloat 组成,且由于 Dart FFI 不支持 Struct 嵌套,所以无法复用 CGPointCGSize 这两个类的代码,只能重复上面的过程重起炉灶。就这样最终将 Objective-C Cocoa API 内建的 Struct 基本都包装成了 Dart 里的类。目前支持的类型有:CGSize,CGPoint,CGVector,CGRect,NSRange,UIOffset,UIEdgeInsets,NSDirectionalEdgeInsetsCGAffineTransform

DartNative Memory Management: C++ Non-Object

dart_native 基于 Dart FFI,通过 C++ 调用 Native 的 API。这种跨多语言的 bridge 就需要考虑到内存管理的问题。上一篇文章 介绍了 Objective-C 对象类型的管理,本篇算是它的续篇,讲下对 structchar * 内存的管理。

如果你还不了解 dart_native 是什么,建议先看下我之前的两篇文章:

PS:dart_objc 已经更名为 dart_native。

问题分析

Cocoa(Touch) 中的好多 API 都用到了系统内建的 structUTF8String(char *) 类型,它们不像 Objective-C 对象那样只存在于堆上(Block 除外),既可以存在堆上也可以在栈上。如果能将 structchar * 用对象的形式包一层,那么就可以将堆上非对象类型的生命周期转换为对象类型,交由 ARC 来管理。由此继续借助上一篇文章的经验和流程,自动释放存储在堆上的 structchar * 类型。

何时销毁非对象类型

首先要确定非对象类型传递的方式。这里的解决方案是全都存储于堆上,并用一个 Wrapper 对象包一层来传递。下面说说为何这么做。

非对象类型如果存储在栈上,那么当调用结束返回后就会被销毁。在跨语言异步调用时,栈上的内存也会被回收,Dart 侧无法长期持有并访问这些数据。Objective-C 大多使用 Block 的方式来实现异步调用逻辑,由于 Block 会去捕获外部变量,所以可以正常运行。

这个 PointerWrapper 类也很简单,它包了个 void *pointer 属性,在析构的时候会释放 pointer 指向的内存:

1
2
3
- (void)dealloc {
free(_pointer);
}

这样就可以把一个非对象类型先 copy 到堆上,然后封装成对象类型来传递了。确保其不会过早被释放,且在同步或异步调用完成后由 ARC 自动释放。

Dart 从 C++ 获取非对象类型

这里分两种情况:

  1. Dart 创建新的 structchar *Pointer<Utf8>)。会通过 Dart FFI 的 allocate 在堆上开辟新的内存,需要释放
  2. Dart 调用 C++ 函数或 Objective-C Block 时获取的返回值。struct 会被拷贝到新创建的堆内存上,需要释放char * 会自动转换成 String不需要释放
  3. C++ 调用 Dart callback 时传入的参数。struct 会被拷贝到新创建的堆内存上,需要释放char * 会自动转换成 String不需要释放

至于如何在 Dart 侧创建诸如 CGRect 之类的 struct,可能又能单开一篇文章来讲了,这里不细说了。Dart 侧并不会直接从 Objective-C/C++ 侧拿到 struct 类型,而是拿到一份 malloc 并拷贝后的指针。

上面这些需要释放的 struct 均可以通过 PointerWrapper 来自动释放,也就是默认创建的是临时变量,用完会自动销毁。Dart 的 struct 都以 NativeStruct 作为基类。其中 addressOf 为指向 struct 的指针,wrapper 接管了 struct 的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class NativeStruct {
Pointer get addressOf;

PointerWrapper _wrapper;
PointerWrapper get wrapper {
if (_wrapper == null) {
_wrapper = PointerWrapper();
}
Pointer<Void> result = addressOf.cast<Void>();
_wrapper.value = result;
return _wrapper;
}

NativeStruct retain() {
wrapper.retain();
return this;
}

release() => wrapper.release();
}

也就是 Dart 获取到的 struct 是个临时变量,不用就会自动销毁。如果需要长期持有,则需要手动 retainrelease。而 Dart 获取到的 char * 则会被自动转为 String 类型,无需关心内存管理。

C++ 从 Dart 获取非对象类型

这里分两种情况:

  1. Dart 调用 C++ 函数或 Objective-C Block 时传入的参数。
  2. Objective-C 调用 Dart callback 时获取的返回值。

Dart 侧的 struct 早已由 PointerWrapper 交给 ARC 来接管生命周期,在调用完成后自动释放。不过需要注意的一点是,Dart 的 String 自动转换为 C++ 的 char *Pointer<Utf8>)时属于新创建 char *需要交给 PointerWrapper 自动释放:

1
2
3
4
5
6
7
dynamic storeCStringToPointer(dynamic object, Pointer<Pointer<Void>> ptr) {
Pointer<Utf8> charPtr = Utf8.toUtf8(object);
PointerWrapper wrapper = PointerWrapper();
wrapper.value = charPtr.cast<Void>();
ptr.cast<Pointer<Utf8>>().value = charPtr;
return wrapper;
}

Dart 向 C++ 传参

  1. 由于字符串比较特殊,即便在函数调用结束后,字符串很多以常量的形式被继续使用。所以传递 char * 的时候,即便已经通过传递 PointerWrapper 来保证调用过程中不被释放,但还需要利用 NSTaggedPointerString 将其生命周期交给 Foundation 管理。
  2. 原本传递结构体现在改成了传递结构体的指针。因为跨语言调用时,使用 Dart FFI 传递单个数据最大为 64bit,可以为整型、浮点型或指针等。所以可能无法容纳下比较大的结构体,需要传递指向结构体的指针。
  3. 其余类型照常传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
_fillArgsToInvocation(NSMethodSignature *signature, void **args, NSInvocation *invocation, NSUInteger offset) {
for (NSUInteger i = offset; i < signature.numberOfArguments; i++) {
const char *argType = [signature getArgumentTypeAtIndex:i];
NSUInteger argsIndex = i - offset;
if (argType[0] == '*') {
// Copy CString to NSTaggedPointerString and transfer it's lifecycle to ARC. Orginal pointer will be freed after function returning.
const char *temp = [NSString stringWithUTF8String:(const char *)args[argsIndex]].UTF8String;
if (temp) {
args[argsIndex] = (void *)temp;
}
}
if (argType[0] == '{') {
// Already put struct in pointer on Dart side.
[invocation setArgument:args[argsIndex] atIndex:i];
} else {
[invocation setArgument:&args[argsIndex] atIndex:i];
}
}
}

Objective-C 调用 Dart callback 时获取的返回值

由于 Dart callback 所对应的 C++ Function 由 libffi 动态创建,而基于动态创建的 C++ Function 又动态创建了 Objective-C Block 和方法。所以这一切都是我们创建的,尽在掌控之中。而这个动态创建的过程又有点复杂,可以再单独开一篇文章来讲了。

Objective-C 中方法和 Block 的返回值如果是比较大的 struct,运行在 x86 架构上时,实际上调用更底层函数时的参数列表会有变化。此时第一个参数是指向返回结构体的指针,其余参数依次后移一位。这在 Objective-C 中缩写为 stret,也就是 struct return 的意思。

如果没有触发 stret 条件,此时的策略是 Dart callback 返回非对象类型锁对应的 PointerWrapper,然后 Objective-C 侧再从 wrapper 中取出对非对象类型,并塞入到 libffi 提供的 ret 指针里:

1
2
3
4
5
6
7
8
9
10
11
12
if (wrapper.hasStret) {
// synchronize stret value from first argument.
[invocation setReturnValue:*(void **)args[0]];
} else if ([wrapper.typeString hasPrefix:@"{"]) {
DOPointerWrapper *pointerWrapper = *(DOPointerWrapper *__strong *)ret;
memcpy(ret, pointerWrapper.pointer, invocation.methodSignature.methodReturnLength);
} else if ([wrapper.typeString hasPrefix:@"*"]) {
DOPointerWrapper *pointerWrapper = *(DOPointerWrapper *__strong *)ret;
const char *origCString = (const char *)pointerWrapper.pointer;
const char *temp = [NSString stringWithUTF8String:origCString].UTF8String;
*(const char **)ret = temp;
}

后记

非对象类型的内存管理要比对象类型复杂得多,光是把 struct 在 Dart 中转换出来就已经有些麻烦了。好在大部分问题都已经克服过去了,最终实现了一套半自动化的内存管理系统,也实现了跨语言的类型自动转换。后续可能还会对 stret 的情况进行优化,甚至对方案进行大改。

哎真是太难了,我还是继续骑着小摩托去找人马吧。

DartNative Memory Management: NSObject

dart_native 基于 Dart FFI,通过 C++ 调用 Native 的 API。这种跨多语言的 bridge 就需要考虑到内存管理的问题。由于篇幅有限,会分开来讲,本篇文章只涉及 Objective-C 对象类型的管理。

如果你还不了解 dart_native 是什么,建议先看下我之前的两篇文章:

问题分析

先看看不同语言是如何管理内存与对象的生命周期的。

  • Dart VM 使用 GC 来管理内存,且 Dart 语言一切皆为对象。
  • C++ 在堆上手动开辟的内存需要手动释放。
  • Objective-C 上的对象普遍使用 ARC 来管理,但也可以使用 MRC。其余跟 C++ 一样。

GC 和引用计数都是常见的内存管理方式,这里就不科普具体算法的细节了。两者差别固然很大,dart_native 在这里做了一些事情,尽量让开发者写 Dart 时少关心内存问题。

由于 Dart 对象的生命周期实际完全由 VM 的 GC 决定,所以这里没有可操作性的空间,只能调整 Objective-C 对象的生命周期。Objective-C 对象都是存储在堆上的,跨语言之间传递的都是指针。而使用栈上的一个 64 位空间也足够存储大部分基本类型数据,足够覆盖到各种长度精度的整型和浮点数类型。

跨语言之间的方法调用,更多关注的是方法返回值给到另一种语言时的生命周期,以及对象被销毁后的处理。

Objective-C 对象销毁后的处理

读过我之前文章的人可能会对 dart_native 的使用方式稍有了解,其实就是自定义 Dart 类来把 Objective-C 类封装了一层。比如我写了个 Dart 类叫 NSObject,封装了大部分基本的 API。打通了方法的调用时类型的自动转换,支持所有基本类型。

Dart 的 NSObject 类有个指向 Objective-C 对象的指针 _ptr,当这个 Objective-C 对象被销毁时,那么对应的 Dart 对象各种状态也需要置空。虽然 Dart 对象没被及时销毁,但是对其的任何操作都是无效的了。当然,这很容易导致难以发现的 bug。所以需要有效地措施来让开发者知道这个 Dart 对象已经失效了。

首先是提供 dealloc 方法,让开发者自己清理子类中的内容,这跟写 MRC 代码很像。
这是基类中 dealloc 方法的实现(简略版),它清空了 _ptr 指针。当 Objective-C 对象被销毁后,dart_native 框架会负责调用 dealloc 方法,开发者不能手动调用。篇幅原因,这部分的实现原理就不展开讲了。

1
2
3
4
5
/// Clean NSObject instance.
/// Subclass can override this method and call release on its dart properties.
dealloc() {
_ptr = nullptr;
}

dealloc 方法被调用后,需要有能够对 Dart 对象判空的能力。于是我创造了个 Dart 版本的 nil,其实就是一个指向 nullptr 的 Dart 对象。

1
final id nil = id(nullptr);

然后重写了 Dart NSObject== 判等方法,使得 NSObject 的判等变成了指针之间的判等。

1
2
3
4
bool operator ==(other) {
if (other == null) return false;
return pointer == other.pointer;
}

如此一来,一旦 Dart 对象内部指向的 Objective-C 对象被销毁,它就等于 nil 了。

Dart 从 Objective-C 获取对象

从 Objective-C 获取对象的方式可能是新创建的,也可能是某个普通方法的返回值。从形式上二者都是调用方法返回对象,但是内存引用计数却不一样。以 new, alloc, copymutableCopy 开头的方法会被认为引用计数加一,这样就相当于把 Objective-C 对象的管理权交给了 Dart。而普通方法返回的 Objective-C 对象的管理权并不归属 Dart。

为了简化操作,让这两种获取方式的结果统一,我会在 Dart 侧 NSObject 基类的这四个相关方法中调用一次 autorelease。这样就又把带 new, alloc, copymutableCopy 前缀的方法返回的 Objective-C 对象的管理权交由 ARC,而又不会过早释放导致 crash。

这里从使用方式可分两种情况:

  1. 临时使用 Objective-C 对象,当为局部变量:Dart 侧编写代码时无需关心内存管理
  2. 长期使用 Objective-C 对象,作为属性持有:Dart 侧需手动 retainrelease

针对第二种情况,写过 MRC 代码的会很熟悉。这是对应的 Dart 代码,是不是很像。

1
2
3
4
5
6
7
8
9
class _MyAppState extends State<MyApp> {
NSObject object = NSObject().retain();
...
@override
void dispose() {
object.release();
super.dispose();
}
}

如果 Dart VM 支持了 finalize,那么现在的『半自动』内存管理就成了『全自动』了,不过那样的话,内存管理方案也会改变。这里就不谈 Plan B 了。

Objective-C 从 Dart 获取对象

dart_native 是支持传入回调方法的,也就是 Objective-C 是可以直接调用 Dart 方法的。当 Objective-C 从 Dart 方法的返回值是对象,需要处理好它的生命周期。

当 Dart 返回给 Objective-C 一个对象时,其内部指向的 Objective-C 对象是交给 ARC 管理的。当 Dart 与 Objective-C 在同一线程时倒还好,切了不同线程后 Objective-C 对象很可能被销毁了,那么就会 crash。此时就需要在 Dart 侧记录下要返回的 Objective-C 对象,这里用到了线程局部存储(TLS)。利用 Dart FFI 调用下面这个 C++ 函数,它在当前线程下持有了 Dart 要返回的 Objective-C 对象,防止被提前销毁。

1
2
3
4
5
6
7
void
native_mark_autoreleasereturn_object(id object) {
int64_t address = (int64_t)object;
[NSThread.currentThread do_performWaitingUntilDone:YES block:^{
NSThread.currentThread.threadDictionary[@(address)] = object;
}];
}

当然还需要在 Objective-C 侧调用完 Dart 方法后,将 TLS 置空,确保不会造成内存泄露。

后记

这篇文章依然没有讲 Dart 如何调用 Objective-C API,没有贴很多代码晒技术细节,满篇都是讲思路和方法。可能是我觉得这些都是 Runtime 的基础,没太多自己思考的东西。写出来也只是简单的科普知识罢了。

张小龙说『思辨大于执行』,当大家都有很强的执行力的时候,先理清思路就显得很重要。

主要还是技术细节太多,几篇文章的篇幅都讲不完,我也懒得一次写完。

谈谈 dart_native 混合编程引擎的设计

我之前在 『用 Dart 来写 Objective-C 代码』 这篇文章讲了下我在解决 Flutter 三端开发问题的一个思路和方案,并给出了 Demo 和简单的对比。这次讲下 dart_native 的设计,这包含了上层使用方式和底层技术方案的设计。由于涉及到的技术点很多,这次不会深入太多技术实现细节,不过后续可能会分篇讲下。

设计思路

宇宙真理①:Native 平台接口随版本变化,差异随时间增长。

  • iOS 有太多的平台独有框架的 CloudKit、PhotoKit、StoreKit …
  • 同理安卓也是,且这些差异都跟 UI 无关,无法通过图形引擎统一。
  • 随着版本发布,不断有新增和废弃的 API,平台差异只会越来越大。

宇宙真理②:任何跨平台开发框架,Native API 该用还得用,可能只是换一种语言封装调用,逃不掉的。

无论是现今炙手可热的 Flutter,还是之前的 RN 和 Weex,都逃不掉这条真理。

还有些跨平台框架不通过 Bridge 或 Channel 调用 Native,而是直接将某种语言代码编译成对应平台的二进制。比如最近出的 Kotlin/Native,或是古老的 Xamarin,也都逃不掉这条真理。

Flutter vs RN/Weex

Flutter 通过图形引擎的跨平台帮我们抹平了 UI 层面的平台差异,这在跨平台开发框架中已经是个突破了。但其余的部分仍然需要开发者编写很多 Channel 代码来抹平不同平台的差异。不妨将二者结合下,取其精华去其糟粕,于是有了一种新的开发方式:

DartNative

为何这样设计

  1. Native API 很多,逐个用 Channel 封装的话要多写很多代码。而这里可以借鉴其他跨平台框架『用同一种语言调用不同平台 API』的成熟经验,以 Dart 语言的形式将 Native API 暴露给 Flutter 来调用。将『三端开发』切换语言和开发环境的场景消灭到最低。
  2. 通过 Native Runtime 来应对不同版本 API 变化问题,以不变应万变。搭配 Dart API 自动化生成工具提升效率,解放手写 Channel 带来的一系列开发成本。

技术指标

一句话:运行性能和研发效率都要吊打 Flutter Channel。

研发效率

以『判断是否安装某 App』为例,针对代码行数进行对比:

代码行数 调试成本
DartObjC Native 1 行/Dart 1 行 dart_native 一行代码直接返回 bool 类型,无需调试 Native 和 Dart 逻辑。
Channel Native 30 行/Dart 15 行 Channel 需定义返回数据格式,手动转换 BOOL 与 int,判断 channel 和 methodName,需要调试 Native 和 Dart 逻辑

由于 dart_native 帮开发者完成了类型自动转换,省去了多余的 Channel 逻辑,也就无需调试这部分代码。只需调试 Dart 代码,统一开发环境和语言。

其实使用 dart_native 后,理论上是不需要写 Native 代码的。

性能数据

分别测试了两个 Native 接口在相同环境下执行 1 万次的耗时情况(ms):

接口案例 总耗时对比(Channel/dart_native) 仅通道耗时对比(Channel/dart_native)
判断是否安装某 App 5202/4166 919/99
打日志 2480/2024 1075/432

严格来讲,对比性能时需要刨除 Native 方法自身的执行耗时,剩下的就是通道的耗时了。在这方面 Flutter Channel 的耗时是 dart_native 的好几倍。在测试打日志这个案例时,dart_native 耗时瓶颈在于将 Dart String 转为 Objective-C NSString,所以耗时仅仅比 Flutter Channel 少了 60% 左右。

而在真实场景下,总耗时就更加有意义。由于 Native 方法本身执行的耗时占比较大,所以最终二者的耗时对比并不是几倍的关系,但 dart_native 依然有着性能上的优势。

支持的特性

为了在 Flutter 中使用,dart_native 无法用到 Dart 反射特性,但依然最大限度地实现了对 Objective-C 语法特性的支持。

内存管理

Dart 和 Objective-C 的内存管理方式差异很大。前者使用 GC,后者使用 ARC。目前的解决方案是『半自动引用计数』的内存管理方式,大多数场景下无需关注内存问题。待 Dart 支持 finalizer 可优化为『全自动』。这其中用到了一些算不上黑科技的土方子,暂且奏效。

Dart 中临时使用和创建的 Objective-C 对象、C-String 或结构体无需关注内存问题,但如果想长期持有,需要调用 retain() 方法,并在不用的时候(比如页面销毁时)调用 release() 方法。

Native Callback

有很多 Native API 的参数一个 Callback。这类方法大多是一些异步返回的方法,传入参数的方式大多是 Block 或 Delegate。为了让 Dart 能够调用这些 API,dart_native 实现了『用 Dart 语法写 Block 和 Delegate』。这需要实现动态创建任意函数签名的 Block 对象和 Objective-C 方法,甚至当 Dart 类并没有对应的 Objective-C 类时,需要动态创建这个类。这其中又涉及到大量内建类型的自动转换和边界问题处理。

多线程 / GCD

Flutter 中运行时,VM 会开辟一些内建的线程来维持 Flutter 的运行。我们编写的 Dart 代码大多跑在 flutter.ui 线程,但这不是 Native 系统的主线程。而有些 API 要求必须在主线程调用,所以 dart_native 也支持指定线程和队列调用。

对于 GCD 的 API 仅有部分支持,且计划为 Swift 风格语法。等 dart:ffi 1.1 支持 async callback 后,这部分的功能会得到加强。

方法调用时的类型自动转换

dart_native 会自动转换 Dart 与 Objective-C 类型。大部分 Objective-C 类型在 Dart 中都有对应的封装类,或者是可以映射到 Dart 基本类型。目前有的转换是单项的,比如 Dart Function 可以转为 Objective-C Block,反之则不行。

已支持以下类型的自动转换:

Dart Objective-C
int int8_t
int int16_t
int int32_t
int int64_t
int uint8_t
int uint16_t
int uint32_t
int uint64_t
char/int/String char
unsigned_char/int/String unsigned char
short/int short
unsigned_short/int unsigned short
long/int long
unsigned_long/int unsigned long
long_long/int long long
unsigned_long_long/int unsigned long long
NSInteger/int NSInteger
NSUInteger/int NSUInteger
size_t/int size_t
float/double float
double double
double CGFloat
bool BOOL/bool/_Bool
CGSize CGSize
CGPoint CGPoint
CGVector CGVector
CGRect CGRect
NSRange NSRange/_NSRange
UIOffset UIOffset
UIEdgeInsets UIEdgeInsets
NSDirectionalEdgeInsets NSDirectionalEdgeInsets
CGAffineTransform CGAffineTransform
NSObject NSObject
NSObjectProtocol NSObjectProtocol
Block/Function NSBlock
Class Class
Selector/SEL Selector/SEL
Protocol Protocol
NSString/String NSString
String char *
Pointer void *
void void
NSValue NSValue
NSNumber NSNumber
NSArray/List NSArray
NSDictionary/Map NSDictionary
NSSet/Set NSSet

用 Dart 来写 Objective-C 代码

这篇文章不是讲 Flutter Channel 入门的,可能让你失望了。不过继续往下读可能也会有点收获。

Flutter 提升了客户端开发的效率,但在跟 Native 代码配合开发时也带来了不好的体验。于是我写了个 Flutter 插件 dart_native,使开发者可以用 Dart 的语法来写 Objective-C 代码。借助于 Flutter 的热重载,也可以更高效的动态调试 Native 代码,从此告别在两个工程和 IDE 中切来切去。方法调用性能相比 Channel 也提升很多。

尚在开发中,开源地址:https://github.com/yulingtianxia/dart_objc

问题背景

先说说为什么会有开发效率的问题。Flutter 的跨平台多适用于 UI 等上层需求,本来是可以提升开发效率的。但是诸如 LBS、系统和设备信息、获取相册等常用功能都需要两端去写很多 Native 代码。最终原本的『两端开发』最后成了『三端开发』。很少会有完全用 Flutter 开发的 App,原因如下:

  1. 一些跟系统和设备强相关的功能只能靠调用 API 来实现
  2. 旧项目引入 Flutter 后需调用已有的 Native 模块代码

既然『三端开发』无法避免,那么增加了哪些成本呢?

  1. 开发过程中需要在至少两个 IDE 打开的工程中来回切换,需单独运行,无论是写代码还是 Debug 都体验不连贯,降低效率
  2. 如果 Flutter 和 Native 代码由不同的人来开发和维护,增加了沟通成本
  3. Flutter 需要通过编写 channel 代码来与 Native 层交互,需要两端开发时统一数据传输协议。不仅 channel 调用性能较差,Model 数据在 Native 与 Flutter 之间传递过程的序列化和反序列化也降低性能。
  4. 通过 channel 在 Flutter 和 Native 之间调用时只支持异步回调

分析问题

既然无法避免调用 Native 的 API,那么就要面对这个事实。下一步是如何能让调用 Native API 的这个过程效率更高。具体体现如下:

  1. 开发效率提高:直接用 Dart 语言在 Flutter 工程里编写和调试代码,无需切换到 Xcode 等其他 IDE 打开的 Native 工程
  2. 运行效率提高:channel 的调用性能差一直被诟病

所以思路就是:

  1. 将 Native API 封装成对应的 Dart 语言,解决一系列语言之间的类型转换和语法兼容问题
  2. 通过一个更高效的方式来调用 Native API,这里使用 dart:ffi 调用 C 函数,再通过 Runtime 机制调用 Native

解决问题

提供一个 Flutter 库来提供 Dart 语言的 API,通过 dart:ffi 作为 Flutter 与 Native 之间的桥。对比 Dart 与 Native 的语法特性,对一些类型进行内存级别的底层转换。

dart_native 由于采用指针地址直接传递的方式,方法调用性能相比 channel 提升了几倍甚至一个数量级。(测试接口为获取系统版本,如涉及复杂参数的序列化可能差异更大)

由于 dart_native 组件还在基于 dev 版本的 Dart 开发,可能后续还会有比较大的变动,甚至是 API 的变化。所以没有过多展开讲实现细节,感兴趣可以去自己看代码:https://github.com/yulingtianxia/dart_objc

使用方法

假如你写了个 Objective-C 的类叫 RuntimeStub,并实现了个 fooBlock: 方法,参数和返回值都是个 block 对象。

1
2
3
4
5
6
7
8
@interface RuntimeStub ()
@end
@implementation RuntimeStub
typedef int(^BarBlock)(NSObject *a);
- (BarBlock)fooBlock:(BarBlock)block {
...
}
@end

利用 dart_native 写 Dart 代码调用过程如下:

初始化一个 NSObject 对象,传入类名就可以 new 任意类型的对象。perform() 方法可以调用任意对象的任何方法,跟 Objective-C 的用法基本一致。

1
2
NSObject stub = NSObject('RuntimeStub');
Block block = stub.perform(Selector('fooBlock:'), args: [barFunc]);

Objective-C 中 Block 这种匿名函数或闭包的概念在 Dart 中其实就是 Function,所以当参数是 Block 对象的时候,可以直接传入一个与之函数签名一样的 Dart Function 对象。dart_native 会自动完成参数类型转换和调用等一系列底层细节。所以用 Dart 实现的 barFunc 与 Objective-C 接口 BarBlock 的签名需要一致:

1
2
3
4
Function barFunc = (NSObject a) {
print('hello block! ${a.toString()}');
return 101;
};

Dart 调用 Block 也很简单,调用 invoke 方法就行:

1
int result = block.invoke([stub]);

最后也可以用 Dart 封装下 RuntimeStub 类,这样调用代码更简洁。这种模板代码后续会做成自动生成的,而不用手写。

1
2
3
4
5
6
7
class RuntimeStub extends NSObject {
RuntimeStub() : super('RuntimeStub');

Block fooBlock(Function func) {
return perform(Selector('fooBlock:'), args: [func]);
}
}

后续

目前的 Cocoa API 封装打算参考 Swift 版本的文档,毕竟 Dart 有些语法跟 Swift 还有点像。

Android 平台的实现也在规划中,最终将会结束 Flutter 三端开发现状,实现真正的前端大一统。

不吹了,还要做的事情真的太多了。。。

❌