普通视图

发现新文章,点击刷新页面。
昨天以前yulingtianxia's blog

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

作者 杨萧玉
2022年12月12日 01:46

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

作者 杨萧玉
2020年10月25日 13:15

看到标题的你可能已经充满疑问: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

作者 杨萧玉
2020年9月28日 17:53

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 内存自动管理

作者 杨萧玉
2020年8月22日 23:05

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 插件

作者 杨萧玉
2020年7月25日 00:04

在上一篇文章《告别 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 仅需一行代码!

作者 杨萧玉
2020年6月25日 16:48

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

作者 杨萧玉
2020年4月25日 15:55

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

作者 杨萧玉
2020年3月28日 15:57

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

作者 杨萧玉
2020年2月24日 00:15

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

作者 杨萧玉
2020年1月31日 23:12

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

作者 杨萧玉
2019年12月26日 12:49

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 混合编程引擎的设计

作者 杨萧玉
2019年11月28日 17:07

我之前在 『用 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 代码

作者 杨萧玉
2019年10月27日 17:59

这篇文章不是讲 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 三端开发现状,实现真正的前端大一统。

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

❌
❌