普通视图

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

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 超级通道,未来还要做的事情还有很多。

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

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

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

一图以蔽之。

BlockHook with Revocation

作者 杨萧玉
2019年5月26日 17:20

BlockHook 开创性地解决了 Objective-C 语言界 Hook Block 的问题,但也迎来了新的问题:

  1. 如何知道某个 Block 对象被谁 Hook 过?
  2. 多次 Hook 的先后顺序?
  3. 如何处理好多次 Hook 同一个 Block 后对任意一次 Hook 的撤销?

这些问题也是开发者在使用时向我反馈过的问题,在这篇文章里,这些问题都将会解决。

关于 BlockHook 的原理,可以先阅读之前的文章:

按顺序构造『虚拟的』 Hook 链表

首先要有一个链表来按先后顺序记录一个 Block 对象上所有的 Hook。这个链表的格式以及持有关系也需要考虑在内。

为此我构造了一个虚拟的链表来记录 Hook 的先后关系,而不是单独创建一个链表显式的记录。首先介绍下 Block 与 token 之间的引用关系:

可以看出每个 BHToken 记录了原始和替换后的 invoke 函数指针,那么先后两次 Hook 就靠 invoke 函数指针来关联了:每个 tokne 的 originalInvoke 就是上一次 Hook 的 token 的 replacementInvoke。而拿到 token 又是靠 Block 对象上的 AssociatedObject,且 key 为 replacementInvoke。这样就构造了一条虚拟的链表:想要获得上次 Hook 的 token,只需在 Hook 的 Block 对象上使用 originalInvoke 作为 key 即可。

下面的代码展示了获取下个 token 的 next 实现。因为链表可能会有新的插入和删除节点操作,所以需确保线程安全。

1
2
3
4
5
6
7
8
9
10
11
- (BHToken *)next
{
BHLock *lock = [self.block bh_lockForKey:@selector(next)];
[lock lock];
if (!_next) {
_next = objc_getAssociatedObject(self.block, self.originInvoke);
}
BHToken *result = _next;
[lock unlock];
return result;
}

这里使用 replacementInvoke 来作为 AssociatedObject 的 key 真是一举多得

  1. 将 token 的生命周期绑定到 Block 对象上,实现 self-managed
  2. 因为函数指针地址是唯一的,确保 Block 上关联每个 token 的 key 不会冲突
  3. Block 的 invoke 指针作为 key,可以找到最后一次 Hook 的 token。进而按 Hook 先后顺序遍历出所有的 token。

下面的代码展示了如何获取最后一次 Hook 的 token。在读取 invoke 函数指针的时候,注意保证线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BHToken *)block_currentHookToken
{
if (![self block_checkValid]) {
return nil;
}
void *invoke = [self block_currentInvokeFunction];
return objc_getAssociatedObject(self, invoke);
}

- (void *)block_currentInvokeFunction
{
struct _BHBlock *bh_block = (__bridge void *)self;
BHLock *lock = [self bh_lockForKey:_cmd];
[lock lock];
void *invoke = bh_block->invoke;
[lock unlock];
return invoke;
}

撤销 Hook

俗话说『请神容易送神难』。好多 Hook 框架只解决的如何 Hook,但是却无法撤销恢复原样,留下一堆烂摊子。

在搭建了 Hook 链表的基础上,多次 Hook 的链表可以简化成 invoke 函数指针之间的关系:

那么撤销 Hook 就可以从链表头部开始遍历,找到当前要 remove 的 token。接着链表上删除这个 token,而这又可以分为两个子问题:

  1. 移除最后一次 Hook:需要将 Block 的 invoke 指针指向 token 的 originalInvoke
  2. 移除最后一次 Hook:需要将上一次 Hook token 的 originalInvoke 指向当前 token 的 originalInvoke

最后肯定还要解除 Block 对象对 token 的持有。

这部分逻辑的实现代码如下,在操作 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
- (BOOL)remove
{
if (self.isStackBlock) {
NSLog(@"Can't remove token for StackBlock!");
return NO;
}
self.deadBlock = nil;
if (self.originInvoke) {
if (self.block) {
BHToken *current = [self.block block_currentHookToken];
BHToken *last = nil;
while (current) {
if (current == self) {
if (last) { // remove middle token
last.originInvoke = self.originInvoke;
last.next = nil;
}
else { // remove head(current) token
BHLock *lock = [self.block bh_lockForKey:@selector(block_currentInvokeFunction)];
[lock lock];
((__bridge struct _BHBlock *)self.block)->invoke = self.originInvoke;
[lock unlock];
}
break;
}
last = current;
current = [current next];
}
}
self.originInvoke = NULL;
objc_setAssociatedObject(self.block, _replacementInvoke, nil, OBJC_ASSOCIATION_RETAIN);
return YES;
}
return NO;
}

总结

BlockHook 还在不断完善每一个细节,尽可能做到有始有终,至善尽美。

❌
❌