利用 Objective-C 语言的 runtime 特性, 通过 Method Swizzling 技术可以实现 hook OC 类的某个方法,但是怎么 hook block 呢?比如想要修改 block 的实现我们该怎么做?
Block 是怎样实现的
首先 Block 也是 OC 对象,它的 isa 指针在初始化时指向 &_NSConcreteStackBlock 或者 &_NSConcreteGlobalBlock,Block 的结构如下:
1 | struct Block_literal_1 { |
了解 Block 的结构之后,再来看下它从定义到运行是如何实现的。以下面这段源码为例:
1 | // example |
终端定位到 main.m 文件目录,利用 Clang 命令,把 main.m 文件转化为 main.cpp 文件,并摘要如下:
1 | clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m |
1 | // file: main.cpp |
可以看到,经过 Clang 转化为c++代码之后,原先5行的源码增加了很多,但我们仍然可以从转化后的代码中找到原始代码的“痕迹”,尽管在形式上已经变得大不相同。
1 | struct __block_impl { |
结构体 __block_impl
清晰的表明了 Block 的结构,Block 也是OC对象,所以它包含 isa
指针;Reserved
作为保留字段;函数指针 FuncPtr
指向 Block 的实现。
1 | struct __main_block_desc_0 { |
结构体 __main_block_impl_0
包含两个变量以及一个构造函数。变量 impl
是 __block_impl
类型;指针变量 Desc
指向 __main_block_desc_0
类型的结构体;构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
第一个参数 fp
是函数指针,指向Block的函数实现。
除了上面提到的三种结构体类型,从转换后的源码中还可以看到一个静态函数 __main_block_func_0
以及一个静态结构体变量 __main_block_desc_0_DATA
。静态函数 __main_block_func_0
承载了 Block 要执行的真正任务;静态结构体变量 __main_block_desc_0_DATA
被初始化时的参数分别是 0 和 结构体 __main_block_impl_0
所占存储空间大小。
了解了 Block 相关的数据结构之后,我们开始看 main 函数中 Block 代码是如何调用运行的。首先是 Block 的定义代码:
1 | int var1 = 11; |
去除类型转换,上面的代码等价于:
1 | int var1 = 11; |
这样就容易理解了,即栈上生成的 __main_block_impl_0
类型结构体实例的指针,复制给 __main_block_impl_0 结构体指针变量 block
。
然后是 Block 的调用代码:
1 | ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, var1); |
去除类型转换,上面的代码等价于:1
(*block ->impl.FuncPtr)(*block, var1)
这就是简单的使用函数指针进行函数调用。
所以,我们只要把 __main_block_impl_0
中的 impl
变量的 FuncPtr
函数指针修改掉,就可以达到替换 Block 所执行的函数的目的。
Hook Block 怎样实现?
上面得出结论,只要把 __main_block_impl_0
中的 impl
变量的 FuncPtr
函数指针修改掉,就可以达到替换 Block 所执行的函数的目的。那么怎么修改 Block 的函数指针呢?跟着下面这个问题来一起看下。
问题1:实现下面的函数,将Block的实现修改成打印 “Hello world.”1
2
3void HookBlockToPrintHelloWorld(id block) {
}
实现如下:
1 | //file:main.m |
问题2: 实现下面的函数,将Block的实现修改成打印所有入参,并调用原始实现.1
2
3
4
5
6void(^block)(int a, NSString *b) = ^(int a, NSString *b) {
NSLog(@"block invoke");
}
HookBlockToPrintArguments(block);
block(123,@"aaa");
//这里输出"123, aaa" 和 "block invoke";
实现如下:
基本思路是将原始 Block 的实现函数,替换成我们自定义的函数,然后将参数打印,最后回调原始 Block 的实现函数。简单实现方法如下(只针对固定的入参,不具备通用性):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//file: main.m
typedef struct hook_Block_Desc {
size_t reserved;
size_t Block_size;
} hook_Block_Desc;
struct hook_Block_impl {
void *isa;
int Flags;
int Reserved;
void *funPtr;
hook_Block_Desc *desc;
};
//函数指针静态变量,保存 Block 的原始函数实现
static void (* orig_func)(void *block, int a, NSString *b);
void hookBlock(void *block, int a, NSString *b) {
NSLog(@"%d, %@",a, b);
orig_func(block, a, b);
};
void HookBlockToPrintArguments(id block) {
struct hook_Block_impl *blockImpl = (__bridge struct hook_Block_impl *)block;
orig_func = blockImpl->funPtr;
blockImpl->funPtr = &hookBlock;
}
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = 123;
NSString *b = @"abc";
void (^block)(int, NSString *) = ^(int a, NSString *b) {
NSLog(@"block invoke.");
};
HookBlockToPrintArguments(block);
block(a, b);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
下面是通用实现方法,因为要接收不确定的参数类型和个数,所以需要循环判断参数类型然后赋值,而且需要深拷贝一份原始 Block 作为参数传递:
1 | struct hook_Block_impl; |
总结
以上介绍了 Block 的数据结构以及 OC 语言是怎样实现 Block 定义与调用的。理解了 Block 的原理,就能够实现 Hook Block,并针对提出的两个问题,做出了解答。