普通视图

发现新文章,点击刷新页面。
昨天以前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 大佬欢迎共建。我是肝不动了,否则博客能断更两年么(手动狗头)?

CPP-Summit 2020

作者 杨萧玉
2020年12月6日 01:16

2020 年是 CPP-Summit 第一次来到深圳,也是赶在了 2020 的尾巴。感谢公司让我有机会参加这种会议见见世面,在此记录下这两天的经历和想法,就当是记笔记交作业了。

简单介绍下这个会议,中文名是『全球 C++ 及系统软件技术大会』。虽然它的英文名里省略了『全球』,但从演讲嘉宾阵容上来看,也算是对得起『全球』二字了:有将近三分之一的演讲者来自国外。官网上的大会主席是『C++ 之父』Bjarne Stroustrup,他今年演讲的主题是《C++ 20 与C++的持续演化》。顺便说下,这个是『真·西佳佳之父』。会议的主办方是一家叫 Boolan 的技术咨询和培训机构,所以会议开始会植入一波硬广,这也算是常规操作了。大会的演讲嘉宾有极少部分是来自这家机构以及大会金主爸爸的,大部分嘉宾都是来自行业内有一定经验的开发者,阵容还算可以。

先说下总体感受:C++ 作为一门亘古不衰的语言,在各行各业都存在着经验丰富的开发者。随着各种更加『现代化』的语言的兴起,C++ 也迎来了更多的变化。不过很多新增语法特性的改进更像是戴着脚镣跳舞,保证对存量代码的向前兼容。在大会上也深深感受到互联网行业对传统软件行业的冲击,甚至讨论了 C++ 这碗饭还香不香的问题。大会的议题总体分为三大类:

  1. 新特性的研究和布道
  2. 性能和安全的实践经验
  3. 研发模式、测试和工程化等方法论

一般这种大会都是分为多个分会场同时演讲的,我只能选其中一些我比较感兴趣的议题去参加,大部分集中在前两类。国外开发者分享的议题大部分集中在第三类,由于今年疫情的原因,都改为线上会议了。国内开发者的分享更侧重于性能和安全,或者尝鲜新特性,尤其是大厂更强调业务落地。这里按时间顺序列出我参加的分享,并附上我的一句话短评:

接下来胡扯下我感受到的几个点。

性能与安全

如果要分析性能,那就一定要涉及到编译器开启的优化选项,不同级别的优化会导致差异很大的结果。有些代码会因为过于简单而被编译器优化没了,从而导致测试结果的偏差。这时候往往需要对照生成的汇编代码来看,推荐一个在线工具:Compiler Explorer

安全方面能做的大都是自动化代码静态扫描工具。C++ 除了接入扫描工具检查之外,也在努力添加新特性,想把一些事情放在编译期去做。好像很多现代化语言都喜欢在编译期保证空值安全,类型安全等等,甚至是编译期就已经计算好了一些常量,推导出了一些结果。想要在编译期做更多事情,就需要语法特性上做更多要求。带来的负面影响就是学习成本高,很难做动态化。比如 Rust,初学者想让自己的代码编译通过都有点难。

C++ 的生命力

C++ 对嵌入式、培训和咨询等行业影响很深,但是移动互联网时代大家更偏向于使用 Go、Python、Rust 等语言。比如有人提到招 C++ 的应届生很难,由于前一阵子深度学习很火,好多都是只会 Python 的候选人。接着也有人回应说深度学习的应用层的确是 Python,但是各种引擎和框架还是需要 C++来实现,这恰恰反映出 C++ 高性能的优势。甚至有人说要是以后大家不学 C++ 了,那么在座的各位待遇就会翻倍了,因为有大量现存的代码是用 C++ 写的,只有我们才看得懂。

大会上好多拥有二三十年 C++ 开发经验的大佬,我这种毕业四年的萌新只能在一旁打 call。大量熟悉底层软硬件的资深开发努力分享经验,做咨询和教育。大家并不希望 C++ 因为过时而构起了壁垒,而是积极拥抱变化,布道更现代化的 C++。

但话又说回来,C++ 的这些新特性很多要照顾已有的旧语法,用起来还是少了内味儿。比如各种现代语言都具有的 Modules 概念,C++至今也并未普及,这也导致 C++ 没有一个公认的制品库管理工具。站在一些现代语言的开发者的视角,C++ 不过是在努力变革求生罢了。尴尬之处就在于,维护 C++ 老代码时没必要升级到新特性(有工作量和风险),若是写新业务还不如直接用上 Go,Java,Scala 等。一门语言的命运啊,当然要靠自我奋斗,但是也考虑到历史的行程。

国内与国外

在一次午餐的时候,我听到隔壁桌聊天说国外开发者讲的东西太虚了。我作为一线开发者深表赞同,也是在『踩坑』后避开了大部分国外开发者的分享。可能这里不仅仅是分享内容不被大家讨喜的原因,主办方的实时翻译效果也不太好。建议主办方考虑下,不要为了追求『全球』国际化而刻意安排国外开发者分享的比例。

为主办方点赞的是现场可以通过贴小心心的方式为分享者打 call。(主办方的小姐姐给了我三颗小心心让我贴给心心比较少的几位,然而我比较耿直,还是送给了我想打 call 的几位分享者)

最受欢迎的当属『C++ 之父』的分享,我觉得这不仅仅是分享内容受欢迎了,『教父』自带的人气和信仰也是很重要的。提问环节甚至有一位华为的项目经理上来就是一句 “Dear father!”,然后在主持人的一通喊停之下来了一通自我介绍,并邀请 Bjarne Stroustrup 来甚至华为参观,想来个拥抱交个朋友。最后说自己没有问题要提问,单纯想膜拜下。(非原话,差不多这个意思)反正把大伙儿都逗笑了。更逗的是线上提问,亮点自寻:

实现 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 官方仓库里,我那时候觉得这个想法真的很大胆。现在看起来,如果完成了上述解决方案的大部分,好像也并不是不可以。

BlockHook and Memory Safety

作者 杨萧玉
2020年5月30日 17:25

BlockHook 最近修复了一些内存安全方面的问题,记录下这些问题的解决思路:

  1. 微信项目使用 BlockHook 时的 MRC 兼容问题
  2. GlobalBlock 在某些场景下的 VM Protection 没有写权限
  3. 如何检测带有 Private Data 的 block

修复 BlockHook 在 MRC 上的问题

ARC 下将 StackBlock 赋值时,会自动 copy 成 MallocBlock。不过这个编译器帮我们做的隐式行为的前提是代码里显示声明为 Block 类型。而 BlockHook 为了能够传入各种签名的 aspectBlock,恰恰用的是 id

1
2
- (nullable BHToken *)block_hookWithMode:(BlockHookMode)mode
usingBlock:(id)aspectBlock;

如果调用方用的是 MRC,即便 BlockHook 是用 ARC 实现的,那么拿到的 aspectBlock 依然是 StackBlock。当被 Hook 的 Block 异步执行时,aspectBlock 也需要异步执行,但它早已经在栈上被释放,进而由于野指针而 crash。

这就是在微信项目里使用 BlockHook 时遇到的问题。当劳动节的下午我正出门去吃饭路上,微信的同事在企业微信上找到了我反馈了这个 bug。我由于路上匆忙没仔细看手机,一开始以为是我另一个同事找我。看问题截图上 Xcode 工程名我还以为他逆向调试微信用了 BlockHook 干啥坏事嘞,于是回了一句『你是真的牛逼』。再定神一看我擦是微信巨佬,虽然贼尴尬但只好装作没事一样继续看问题。。。扯远了。。。

微信巨佬果然是巨佬,还给了我解决方案。我照着巨佬给的思路,copy 了传入的 aspectBlock

1
2
// If aspectBlock is a NSStackBlock and invoked asynchronously, it will cause a wild pointer. We copy it.
_aspectBlock = [aspectBlock copy];

解决 GlobalBlock 没有写权限的问题

用 Xcode 11 编译时,将 Deployment Info 中的 target 选择 iOS 13 后,GlobalBlock 对象所占的内存是只读的,这就导致 Hook 过程中无法对 invoke 函数指针做写操作,直接 crash。

首先需要判断下 invoke 指针对应的地址有没有写权限,如果没有写权限则需要提权。这涉及到 VM Region 和 Protection 的一些操作,在获取内存地址的基本信息时也要注意区分下 64 位和 32 位:

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
static vm_prot_t ProtectInvokeVMIfNeed(void *address) {
vm_address_t addr = (vm_address_t)address;
vm_size_t vmsize = 0;
mach_port_t object = 0;
#if defined(__LP64__) && __LP64__
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t ret = vm_region_64(mach_task_self(), &addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
#else
vm_region_basic_info_data_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT;
kern_return_t ret = vm_region(mach_task_self(), &addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
#endif
if (ret != KERN_SUCCESS) {
NSLog(@"vm_region block invoke pointer failed! ret:%d, addr:%p", ret, address);
return VM_PROT_NONE;
}
vm_prot_t protection = info.protection;
if ((protection&VM_PROT_WRITE) == 0) {
ret = vm_protect(mach_task_self(), (vm_address_t)address, sizeof(address), false, protection|VM_PROT_WRITE);
if (ret != KERN_SUCCESS) {
NSLog(@"vm_protect block invoke pointer VM_PROT_WRITE failed! ret:%d, addr:%p", ret, address);
return VM_PROT_NONE;
}
}
return protection;
}

在修改 invoke 指针后,还需要恢复原来的权限。相当于我只是在需要替换 invoke 指针的时候临时开了写权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static BOOL ReplaceBlockInvoke(struct _BHBlock *block, void *replacement) {
void *address = &(block->invoke);
vm_prot_t origProtection = ProtectInvokeVMIfNeed(address);
if (origProtection == VM_PROT_NONE) {
return NO;
}
block->invoke = replacement;
if ((origProtection&VM_PROT_WRITE) == 0) {
kern_return_t ret = vm_protect(mach_task_self(), (vm_address_t)address, sizeof(address), false, origProtection);
if (ret != KERN_SUCCESS) {
NSLog(@"vm_protect block invoke pointer REVERT failed! ret:%d, addr:%p", ret, address);
}
}
return YES;
}

虽然我还没花时间去追查苹果爸爸为啥要在 Xcode 11 上 iOS 13 target 编译时给 GlobalBlock 只读权限,但理论上我的这个操作并不是对非法内存地址的提权,应该是被允许的,毕竟线上检测是否越狱等功能也会用到这些 API。但我还是不放心,请教了页面仔大佬后,答复是可以上架,终于安心了,也期待下个版本可以试试。

如果有大佬知道苹果爸爸为何会这样做,或者有更优雅更安全的方案,请给小弟赐教,欢迎指出缺陷,一起开源共建。

优化 BlockHook 检测 Private Data 的方式

BlockHook with Private Data 这篇文章里我曾经介绍过一种『骨骼惊奇』的 Block,不能直接替换 invoke 函数指针来 Hook。当时判断这类带有 Private Data 的 Block 的依据是直接用 Private Data 中的 dbpd_magic 字段与 DISPATCH_BLOCK_PRIVATE_DATA_MAGIC 判等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DISPATCH_ALWAYS_INLINE
static inline dispatch_block_private_data_t
bh_dispatch_block_get_private_data(struct _BHBlock *block)
{
// Keep in sync with _dispatch_block_create implementation
uint8_t *x = (uint8_t *)block;
// x points to base of struct Block_layout
x += sizeof(struct _BHBlock);
// x points to base of captured dispatch_block_private_data_s object
dispatch_block_private_data_t dbpd = (dispatch_block_private_data_t)x;
if (dbpd->dbpd_magic != DISPATCH_BLOCK_PRIVATE_DATA_MAGIC) {
return nil;
}
return dbpd;
}

我知道这种暴力 Memory Overflow 的行为有潜在隐患,而且调试时开启了 Address Sanitizer 后会必现 crash。当时这么做的原因我也在文章里写了,GCD 源码中会检查 Block 的 invoke 指针是否为 _dispatch_block_special_invoke,以此判断 Block 是否包含 Private Data。而这个标志位指针是私有的,我无法在没有符号表的场景下获取到。现在想想当时的自己真是个 SB,当初这么简单的问题,其实现在换个思路不就解决了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DISPATCH_ALWAYS_INLINE
static inline dispatch_block_private_data_t
bh_dispatch_block_get_private_data(struct _BHBlock *block) {
if (!blockWithPrivateData) {
blockWithPrivateData = dispatch_block_create(0, ^{});
}
if (block->invoke != ((__bridge struct _BHBlock *)blockWithPrivateData)->invoke) {
return nil;
}
// Keep in sync with _dispatch_block_create implementation
uint8_t *privateData = (uint8_t *)block;
// privateData points to base of struct Block_layout
privateData += sizeof(struct _BHBlock);
// privateData points to base of captured dispatch_block_private_data_s object
dispatch_block_private_data_t dbpd = (dispatch_block_private_data_t)privateData;
if (dbpd->dbpd_magic != DISPATCH_BLOCK_PRIVATE_DATA_MAGIC) {
return nil;
}
return dbpd;
}

既然无法直接拿到 _dispatch_block_special_invoke 指针,那我干脆创建一个带有 Private Data 的 Block 然后取它的 invoke 指针不就搞定了吗!现在看看当初的自己好傻啊。

最后谈谈 BlockHook

其实 BlockHook 的诞生纯属偶然,起初是我本想做些其他关于 Block 的事情,但技术太菜一直没搞成。一顿瞎折腾失败后,剩余的代码就是 BlockHook 的雏形。然后业余时间不断踩坑和填坑,收到用户反馈后不断打磨,最终搞出了个能用的版本。我本以为打磨了这么久,应该没啥大问题了,然而还是不断有新的问题和挑战出现。毕竟自己曾经吹下了牛皮,含着泪也要继续打磨下去。有时候兴趣带来的动力真的远超 KPI 的压力,让人干劲十足,哈哈。

我曾经吹牛说 BlockHook 『(应该是)填补了 Objective-C 业界在 Hook Block 技术领域的空白』,后来五子棋跟我说之前肯定有人做过这件事,不过记不清是哪个项目了。我也很想知道在这之前是否有人 Hook 过 Objective-C 的 Block,也跪求打脸并虚心接受。但我对 Hook 的理解并不是局限于替换个函数指针 IMP 就可以了,我个人觉得能配得上是 Hook/AOP 的框架,至少要满足下面几个要求中的大部分吧:

  1. 用同一个 Hook 框架多次 Hook,能够有完整的 Hook 调用链。甚至能兼容其他框架。
  2. 兼容 90% 以上的使用场景,经得住大规模验证(不一定线上,也可以是作为测试工具)。
  3. 不能为了『轻量级』和高性能而去牺牲兼容性、鲁棒性和易用性,否则就是实现度不够。
  4. 支持 Revert Hook,最好能 Revert Hook 链的中间节点,甚至能完美还原现场。

其实替换个函数指针并用 libffi 调用任意函数之类的事情随便找个人都会很快上手,如果就只做了这点事情我个人是不敢称其为 Hook/AOP 框架的。BlockHook 的大部分内容都是解决上面所列出的几点要求,并且自认为解决的还算不错。所以 BlockHook 是否填补了业界空白,就看大佬们如何看待 Hook 这件事情的定义了。PS: 可能会误伤一些人,千万别对号入座啊。我也曾经搞过『轻量级』的轮子,性能也牛逼,其实问题一堆实现度很低。我其实在吐槽我自己。。。

最后,跪求苹果爸爸别搞事情了。。。

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 三端开发现状,实现真正的前端大一统。

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

App 二进制文件重排已经被玩坏了

作者 杨萧玉
2019年9月1日 00:47

『二进制文件重排优化启动速度』本是一项上古 PC 时代就玩过的东东,前一阵子借助某宇宙大厂重新火了一把。不过令我惊讶的是:这么简单个事情竟然搞得如此复杂,而且还声称『开拓性的探索、在没有业界经验可供参考』。。。

说真话可能会得罪人,但是我怕过吗? 我怂了,这段掐了。

其实二进制文件重排很简单啊,重点在于生成 order 文件。我基于 Clang SanitizerCoverage 和业界已有的经验,整了个 AppOrderFiles,一个调用搞定!Enjoy it!

1
2
3
AppOrderFiles(^(NSString *orderFilePath) {
NSLog(@"OrderFilePath:%@", orderFilePath);
});

苹果官方文档的古老方案

苹果的官方文档很早就给了二进制文件重排的方案:Improving Locality of Reference,『早』到甚至被苹果提示这份文档已经年久失修,部分工具和链接失效了。文档的过时不仅体现在还是 GCC 时代,连工具链比如像 gprof 也不能用了,不过 Google 也给出了 macOS 上的替代品,有兴趣的可以去研究下。

Facebook 的 hfsort

需要先用 hf-prod-collect.sh 收集数据,然后塞给 hfsort 生成 hotfuncs.txt 文件。很好很强大,不过对于编程小白来说有一定的使用成本。

PS:此方案来自于我写了这篇文章后,jmpews 大神丢给我了个链接,受益匪浅。(其实我啥都看不懂)

基于 Clang SanitizerCoverage 的方案

Clang 10 documentation 中可以看到 LLVM 官方对 SanitizerCoverage 的详细介绍,包含了示例代码。

简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以 __sanitizer_cov_trace_pc_ 为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。

开启 SanitizerCoverage 的方法是:在 build settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard。如果含有 Swift 代码的话,还需要在 “Other Swift Flags” 中加入 -sanitize-coverage=func-sanitize=undefined。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用。

基于 Clang SanitizerCoverage 我写了个工具 AppOrderFiles。CocoaPods 接入,一行调用生成 Order File。啥也不说了,全在 GayHub 里了:https://github.com/yulingtianxia/AppOrderFiles

当然这也不完全是我的原创,对照着 Clang 文档的同时,还参考了 Improving App Performance with Order Files 这篇文章的代码。人家这篇文章虽然早就给出了,不过还是有一些 bug 和优化空间的。

原理就是在 SanitizerCoverage 的回调函数里将地址先收集到队列里,调用 AppOrderFiles() 后会停止收集,并将队列中的 PC 地址依次翻译符号,最后去重。反正代码也不多,直接贴核心代码:

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
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;

static BOOL collectFinished = NO;

typedef struct {
void *pc;
void *next;
} PCNode;

// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint32_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
// if(*guard)
// __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
// __sanitizer_cov_trace_pc_guard(guard);
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
if (collectFinished) {
return;
}
// If you set *guard to 0 this code will not be called again for this edge.
// Now you can get the PC and do whatever you want:
// store it somewhere or symbolize it and print right away.
// The values of `*guard` are as you set them in
// __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
// and use them to dereference an array or a bit vector.
*guard = 0;
void *PC = __builtin_return_address(0);
PCNode *node = malloc(sizeof(PCNode));
*node = (PCNode){PC, NULL};
OSAtomicEnqueue(&queue, node, offsetof(PCNode, next));
}

后记

苹果官方也提供了 PGO 的详细文档,而且操作很简单。不过它跟二进制文件重排还是有区别的,这里不展开讲了。毕竟相对于对业务代码加载优先级的优化来说,PGO 对启动优化性价比没那么高,应该就是高频调用函数内联之类的(这句纯属瞎扯)。

我为啥过了这么久才发此文呢?猜猜原因是啥:

A. 不爱蹭热度
B. 喜欢炒冷饭
C. 忙准备答辩
D. 8 月已经发过文章了,这篇得等到 9 月发,这样才不浪费

碰到不会的题,我一般三短一长选最长。

BlockHook with Invocation(2)

作者 杨萧玉
2019年8月11日 22:18

上一篇文章 简单介绍了下 retainArgumentsblock_interceptor 实现的思路,本文会详细讲解下 BHInvocation 的接口设计与实现,并与系统的 NSInvocation 作对比。

接口设计

BHInvocation 相当于是参照 NSInvocation 的接口并改造了下,以承载 BlockHook 的一些元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface BHInvocation : NSObject

@property (nonatomic, readonly, weak) BHToken *token;
@property (nonatomic, readonly) BlockHookMode mode;
@property (nonatomic, getter=isArgumentsRetained, readonly) BOOL argumentsRetained;
@property (nonatomic, strong, readonly) NSMethodSignature *methodSignature;
- (void)invokeOriginalBlock; // 替代 invoke 和 invokeWithTarget:
- (void)retainArguments;
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

@end

BlockHook 相关的接口

为了存储 Hook 相关的信息,需要在 NSInvocation 的接口基础上新增 token 属性和 mode 属性。不过 BHToken 其实已经存储了 BlockHookMode,为何还要再在 BHInvocation 中加一个 mode 呢?

BHToken 存储的是一次 Hook 行为的元数据;BHInvocation 存储的是 Hook 后 Block 执行时的元数据。BHToken 存储的 mode 是 Hook 的模式,可能包含了多种模式;而 BHInvocation 存储的 mode 则是当前这次 Hook 执行回调所处的时机。

例如同时 Hook Block 执行的前后,此时传入的 mode 值为 BlockHookModeBefore|BlockHookModeAfter,生成的 BHToken 的值也是一样。而 Block 执行前后会有两次回调,传入的 BHInvocation 参数内容却不太一样:其 mode 分别为 BlockHookModeBeforeBlockHookModeAfter。但这两次传入的 BHInvocation 中的 token 确是完全一样。

BHToken 也是初始化 BHInvocation 所用到的唯一参数。

由于是 Hook,所以执行 Block 时需要注意是调用原始实现还是新的实现。 加入了 invokeOriginalBlock 接口来调用原始实现,这也是所有 AOP 工具的必要设计。

NSInvocation 相关的接口

为了降低使用者的学习成本,BlockHook 的接口设计上会尽量参照一些已有的 AOP 工具。在 Invocation 这块,能参照的最好的例子就是系统提供的 NSInvocation。其提供了读、写和 retian 参数列表/返回值的接口,以及方法签名等。

NSInvocation 有些接口在 BlockHook 中是用不到的,比如 selector 属性没什么意义,再比如 invokeinvokeWithTarget: 这两个接口在 AOP 场景下也不必存在。

接口实现

上一篇文章中介绍了过了 retainArguments 的实现思路,针对每个指向参数或返回值的指针都需要经历 “Copy” 和 “Retain” 两步:

Copy Pointer

无论 pointer 指向的内容是一个 struct 还是 NSObject *,都需要将 pointer 的内容拷贝,防止原始内存被修改或者释放。在拷贝前需要开辟新的内存,其生命周期与 BHInvocation 绑定在一起。

1
2
3
4
5
6
7
8
9
10
- (void *)_copyPointer:(void **)pointer encode:(const char *)encode key:(NSNumber *)key
{
NSUInteger pointerSize;
NSGetSizeAndAlignment(encode, &pointerSize, NULL);
NSMutableData *pointerData = [NSMutableData dataWithLength:pointerSize];
self.mallocMap[key] = pointerData;
void *pointerBuf = pointerData.mutableBytes;
memcpy(pointerBuf, pointer, pointerSize);
return pointerBuf;
}

Retain Pointer

如果 pointer 指向的内容依然是个指针,比如 NSObject *char *,还需要防止其内容提前被释放,产生野指针。这里相当于是对 Objective-C 对象和 C-String 的特殊处理,以参数和返回值的 index 作为 key,利用字典 retainMap 强引用 Objective-C 对象;对于 Block 对象还需调用 copy 方法,将栈上的 Block 拷贝到堆上防止被提早释放;对于 C-String 则是开辟新内存并拷贝字符串内容,然后放入 retainMap 中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)_retainPointer:(void **)pointer encode:(const char *)encode key:(NSNumber *)key
{
void *p = *pointer;
if (!p) {
return;
}
if (encode[0] == '@') {
id arg = (__bridge id)p;
if (strcmp(encode, "@?") == 0) {
self.retainMap[key] = [arg copy];
}
else {
self.retainMap[key] = arg;
}
}
else if (encode[0] == '*') {
char *arg = p;
NSMutableData *data = [NSMutableData dataWithLength:sizeof(char) * strlen(arg)];
self.retainMap[key] = data;
char *str = data.mutableBytes;
strcpy(str, arg);
*pointer = str;
}
}

读写参数和返回值

上一篇文章讲述了 BHInvocation 存储参数列表和返回值上的一些处理策略,这里来讲讲如何读写。

在实现读写参数列表和返回值接口时,不仅仅是对 argsretValue 指针的读写操作,还要考虑到 Copy Pointer 和 Retain Pointer。

Copy Pointer 这步无需自行开辟内存了,原因是写入时 retainArguments 的时候已经开辟好了,读取时直接使用传入的指针。

Retain Pointer 接口使用 idx 作为 key,写入新的值时会替换字典 retainMap 中的旧值。这样既可以释放旧值,也能重新 retain 新值。

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
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx
{
if (!argumentLocation || !self.args || !self.args[idx]) {
return;
}
void *arg = self.args[idx];
const char *type = [self.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);
memcpy(argumentLocation, arg, argSize);
}

- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx
{
if (!argumentLocation || !self.args || !self.args[idx]) {
return;
}
void *arg = self.args[idx];
const char *type = [self.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);
if (self.isArgumentsRetained) {
[self _retainPointer:argumentLocation encode:type key:@(idx)];
}
memcpy(arg, argumentLocation, argSize);
}

在 ARC 下从 NSInvocation 读取参数或返回值时,如果类型为 Objective-C 对象,则需要避免默认的强引用。Stack Overflow 上有具体解决方案,其中的一种方案如下:

1
2
NSObject * __unsafe_unretained arg;
[invocation getArgument:&arg atIndex:1];

BHInvocation 由于高仿了 NSInvocation 的接口和实现,所以也需要注意此问题。究其原因在于 memcpy 只是内存拷贝,不是直接向 strong 类型变量赋值,并不会参与到 ARC 的引用计数中。而出了作用域后 ARC 会自动对 strong 类型 release 一次,导致读取到的对象过度释放,导致 crash。(PS:ARC 真实的实现机制会更复杂些,为了描述方便这里对原理进行了简化)

其实还有一种更好的方式读参数,那就是直接在 aspectBlock 中取参数。aspectBlock 中的参数是可以随意写的,但需要跟 Block 的参数列表对应上。写法可以参照下面这个测试用例,直接获取参数,然后修改参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)testObjectArg {
NSObject *argOrig = [NSObject new];
NSObject *argFixed = [NSObject new];
void (^ObjectArgBlock)(NSObject *) = ^(NSObject *test)
{
NSAssert(test == argFixed, @"Modify struct member failed!");
};

[ObjectArgBlock block_hookWithMode:BlockHookModeBefore usingBlock:^(BHInvocation *invocation, NSObject *test){
NSAssert(test == argOrig, @"Wrong arg!");
// Hook 改参数
[invocation setArgument:(void *)&argFixed atIndex:1];
}];

ObjectArgBlock(argOrig);
}

总结

最初 BHInvocation 还不够完善时,读写 Block 的参数/返回值只能用二级指针之类的晦涩语法直接操作 argsretValue,门槛较高而且还不够安全。BHInvocation 接口设计和实现上尽量参考已有的成熟案例,降低开发者学习成本,快速上手。

BlockHook with Invocation(1)

作者 杨萧玉
2019年7月27日 17:14

BlockHook 在业界已经率先解决了在同步调用场景下对 Objective-C Block 的 AOP 问题。但也有很多场景是需要先调用一段自己的逻辑,然后再异步延时执行 Block。

比如从外部跳转到 App 某个页面前需要检查下登录态,如果未登录则需要走完登录流程后才能继续跳转页面,而几乎所有基于 Block callback 的路由组件都没提供路由拦截器的功能。不同的路由组件内部实现不同,想要实现拦截器就需要针对不同的内部实现来修改路由组件源码。

因此我实现了 BlockHook 的异步拦截功能,所有基于 Block 的路由组件就都有了通用的路由拦截器!

当然,Block 拦截器的应用场景不仅于此。只要是需要『同步改异步执行』 Block 的场景都可以用到。

让子弹再飞一会儿!

使用方法

BlockHook 拦截器用法很简单,在已有 BHInvocation 参数的基础上,增加了一个 completion 回调。当拦截器的逻辑异步执行完后,调用 completion 即可继续执行原来的 Block。如果拦截器的逻辑是同步的,也依然可以用这个接口,只是没必要罢了,推荐直接用原来的 block_hookWithMode:usingBlock: 接口。

1
2
3
4
5
6
7
8
9
10
typedef void(^IntercepterCompletion)(void);

/**
Interceptor for blocks. When your interceptor completed, call `completion` callback.
You can call `completion` asynchronously!

@param interceptor You **MUST** call `completion` callback in interceptor, unless you want to cancel invocation.
@return BHToken instance.
*/
- (BHToken *)block_interceptor:(void (^)(BHInvocation *invocation, IntercepterCompletion completion))interceptor;

举个例子,拦截时修改传入的参数,并延迟 0.5 秒再执行 Block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSObject *testArg = [NSObject new];
NSObject *testArg1 = [NSObject new];

NSObject *(^testblock)(NSObject *) = ^(NSObject *a) {
return [NSObject new];
};

[testblock block_interceptor:^(BHInvocation *invocation, IntercepterCompletion _Nonnull completion) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
*(void **)(invocation.args[1]) = (__bridge void *)(testArg1);
completion();
});
}];

testblock(testArg);

实现原理

首先想想如果要延迟一个 Objective-C 方法的执行,需要怎么做?

答案是利用消息转发机制,NSInvocation 调用 retainArguments 将方法执行所需的上下文持有,这样才能保证方法执行时所需的参数、target 等不会被释放。

对于 Block 来说,虽然也能通过 NSInvocation 来进行调用,但是经过 Hook 过后已经不再适用。因为 NSInvocation 的实现机制以及生命周期管理是个黑盒,且无法承载 Hook 相关的信息,需要自己来实现个 BHInvocation

BHInvocation 结构

我之前的 BlockHook with Struct 这篇文章提到了个技术点:在 x86 架构下,当 Block 返回值是大于 16 Byte 的 struct 时,参数列表有些变化:

为了兼容这种情况,需要两套 argsretValue。一套『真的』用于传给 libffi 调用原始函数指针,另一套『假的』提供给使用方读写参数和返回值。这样使用方无需关心底层特殊逻辑,直接用就行了。

BHInvocation 主要结构如下:

PS:BHInvocationNSInvocation 的场景和用法有些不同,所以实现上也会有差异。NSInvocation 没有公开源码,想了解原理的可以看看 mikeash 的实现: MAInvocation。但我并没有参考过 mikeash 的源码,因为等我写完了才发现它。。。

retainArguments 实现

retainArguments 实现策略:

  1. 拷贝 void **args 指针数组和返回值指针
  2. retain 指针内容类型为 Objective-C 对象的参数
  3. 如果参数中也有其他 Block 对象,则 copy 过来
  4. 如果参数中有 C-string,则 strcpy 过来

需要注意的是这里依然要考虑两套 argsretValue 的问题。代码就不贴了,有兴趣的可以自己去看。

block_interceptor 实现

解决了 retainArguments 的实现,一切都好说了。只要基于原有的 block_hookWithMode:usingBlock: 接口稍加改装即可:

1
2
3
4
5
6
7
8
9
10
11
- (BHToken *)block_interceptor:(void (^)(BHInvocation *invocation, IntercepterCompletion completion))interceptor {
return [self block_hookWithMode:BlockHookModeInstead usingBlock:^(BHInvocation *invocation) {
if (interceptor) {
IntercepterCompletion completion = ^() {
[invocation invokeOriginalBlock];
};
interceptor(invocation, completion);
[invocation retainArguments];
}
}];
}

后记

写了这么多关于 BlockHook 的文章,我越来越发现自己在苹果爸爸面前所表现出的无知。几乎每一步都要去踩很多坑,看很多源码。而这次是看着苹果爸爸的文档脑补如何实现,业界也没有能参考的先例。

这种感觉犹如自己在黑暗中不断探索,并享受着这种孤独。

标题暗示着,这篇文章可能会有后续的。

BlockHook with Private Data

作者 杨萧玉
2019年6月19日 22:50

在使用 BlockHook Hook 所有 Block 对象时,发现有些 Block 被 Hook 后会 Crash。究其原因发现是它们骨骼惊奇,夹带了很多『私货』,不能直接 Hook!本文讲述 BlockHook 在处理这种 Block 时的技术原理,解开含有 Private Data 的 Block 的神秘面纱。

由于关于 Block Private Data 的资料几乎没有,所以我完全可以当回标题党,把这篇文章的标题叫做『你真的了解 Block 么?』或者『这才是 Hook Block 的正确姿势』之类的。但想想还是算了吧,怕被大佬们嘲笑称又『改变业界』了啊。

Block 为何会有 Private Data

首先来看一段代码:

1
2
3
dispatch_block_t block = dispatch_block_create(0, ^{
NSLog(@"I'm dispatch_block_t");
});

dispatch_block_create 创建的 Block 都很特殊,返回的 Block 包含了参数里传入的 Block。此时 dispatch_block_t 虽然表面上是一种普通的 Block,但它的构造暗藏玄机,含有 Private Data,下面会详细解读。

特殊的 invoke 函数

这种 Block 的 invoke 函数指针是固定的,函数名为 ___dispatch_block_create_block_invoke。在 linux 系统下,函数名为 __dispatch_block_create_block_invoke,嗯少了个下划线。这个函数的定义来自 libdispatch.dylib,也就是我们常用的 GCD。

1
2
3
4
5
6
7
8
9
10
11
extern "C" {
// The compiler hides the name of the function it generates, and changes it if
// we try to reference it directly, but the linker still sees it.
extern void DISPATCH_BLOCK_SPECIAL_INVOKE(void *)
#ifdef __linux__
asm("___dispatch_block_create_block_invoke");
#else
asm("____dispatch_block_create_block_invoke");
#endif
void (*const _dispatch_block_special_invoke)(void*) = DISPATCH_BLOCK_SPECIAL_INVOKE;
}

libdispatch 会通过判断 Block 的 invoke 指针是否为 _dispatch_block_special_invoke,来知道这个 Block 是否含有 Private Data。

1
2
3
4
5
6
DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_block_has_private_data(const dispatch_block_t block)
{
return (_dispatch_Block_invoke(block) == _dispatch_block_special_invoke);
}

不幸的是,_dispatch_block_special_invoke 是私有的。在非调试场景下是无法通过 dladdr 等方式来获取它的函数名的。也就无法用类似上面的代码来判断 Block 是否含有 Private Data 了。

获取 Private Data

使用 dispatch_block_create 创建的 dispatch_block_t 只是个『壳』,真正执行的是其内部包含的 Block。再加上 GCD 所需的一些数据(queue,group,thread,priority 等),这些数据都需要作为 Private Data 追加在 Block 上。对实现 BlockHook 来说最需要关注的就是 dbpd_magicdbpd_block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OS_OBJECT_DECL_CLASS(voucher);

struct dispatch_block_private_data_s {
unsigned long dbpd_magic;
dispatch_block_flags_t dbpd_flags;
unsigned int volatile dbpd_atomic_flags;
int volatile dbpd_performed;
unsigned long dbpd_priority;
voucher_t dbpd_voucher;
dispatch_block_t dbpd_block;
dispatch_group_t dbpd_group;
dispatch_queue_t dbpd_queue;
mach_port_t dbpd_thread;
};
typedef struct dispatch_block_private_data_s *dispatch_block_private_data_t;

既然无法用 _dispatch_block_special_invoke 来判断 Block 是否含有 Private Data,可以使用 dbpd_magic 魔数来判断。当其值为 0xD159B10C 时(DisBloc 的意思),则表明含有 Private Data。当然这种溢出的方式同样是有风险的,但触及到 PAGEZERO 概率很低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define DISPATCH_BLOCK_PRIVATE_DATA_MAGIC 0xD159B10C // 0xDISPatch_BLOCk

DISPATCH_ALWAYS_INLINE
static inline dispatch_block_private_data_t
bh_dispatch_block_get_private_data(struct _BHBlock *block)
{
// Keep in sync with _dispatch_block_create implementation
uint8_t *x = (uint8_t *)block;
// x points to base of struct Block_layout
x += sizeof(struct _BHBlock);
// x points to base of captured dispatch_block_private_data_s object
dispatch_block_private_data_t dbpd = (dispatch_block_private_data_t)x;
if (dbpd->dbpd_magic != DISPATCH_BLOCK_PRIVATE_DATA_MAGIC) {
return nil;
}
return dbpd;
}

最后真正执行的其实是 dbpd_block 这个 Block,dispatch_block_t 只是个保存各种元数据的壳。

适配 BlockHook

虽然说 Private Data 本身并不是 Block 实现中必要的一环,它只是 GCD 对 Block 数据结构的一种『魔改』扩充。但由于 GCD 内部的一些保护机制,会在修改了 Block 的 invoke 指针后触发 crash(__builtin_trap),所以不能直接对含有 Private Data 的 Block 进行 Hook。这就需要 BlockHook 组件做一些适配工作。

Hook 真正要执行的 Block

既然 dbpd_block 才是真正要执行的 Block,那么 Hook 的时候需要先获取 Private Data,然后对其 dbpd_block 进行 Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BHToken *)block_hookWithMode:(BlockHookMode)mode
usingBlock:(id)aspectBlock
{
if (!aspectBlock || ![self block_checkValid]) {
return nil;
}
struct _BHBlock *bh_block = (__bridge void *)self;
if (!_bh_Block_descriptor_3(bh_block)) {
NSLog(@"Block has no signature! Required ABI.2010.3.16. %@", self);
return nil;
}
// Handle blocks have private data.
dispatch_block_private_data_t dbpd = bh_dispatch_block_get_private_data(bh_block);
if (dbpd && dbpd->dbpd_block) {
return [dbpd->dbpd_block block_hookWithMode:mode usingBlock:aspectBlock];
}
return [[BHToken alloc] initWithBlock:self mode:mode aspectBlockBlock:aspectBlock];
}

获取 Block 当前 Hook Token

因为 Hook 的是 dbpd_block,所以获取 Token 的时候也需要额外处理下。要在 dbpd_block 上通过 AssociatedObject 来获取 Token,而不是 dispatch_block_t 上。

1
2
3
4
5
6
7
8
9
10
11
12
- (BHToken *)block_currentHookToken
{
if (![self block_checkValid]) {
return nil;
}
dispatch_block_private_data_t dbpd = bh_dispatch_block_get_private_data((__bridge struct _BHBlock *)(self));
if (dbpd && dbpd->dbpd_block) {
return [dbpd->dbpd_block block_currentHookToken];
}
void *invoke = [self block_currentInvokeFunction];
return objc_getAssociatedObject(self, invoke);
}

总结

代码地址: https://github.com/yulingtianxia/BlockHook

一图以蔽之。

❌
❌