iOS Block
iOS Block用法和实现原理
Block概要
Block:
带有自动变量的匿名函数。
匿名函数
没有函数名的函数,一对{}包裹的内容是匿名函数的作用域。
自动变量
虽然使用Block不用声明类,但是Block提供了类似Objective-C的类一样可以通过成员变量来保存作用域外变量值的方法,那些在Block的一对{}里使用到但却是在{}作用域以外声明的变量,就是Block截获的自动变量。
Block常规概念
Block语法
1 | ^ 返回值类型 (参数列表) {表达式} |
例如:1
2
3^ int (int count) {
return count + 1;
};
其中,可省略部分有:
返回类型,例:
1 | ^ (int count) { |
参数列表为空,则可省略,例:
1 | ^ { |
Block类型变量
声明Block类型变量语法:1
返回值类型 (^变量名)(参数列表) = Block表达式
例如:1
2
3int (^blk)(int) = ^(int count) {
return count + 1;
};
声明类对象的Block类型属性语法:1
@property(修饰符) 返回值类型 (^变量名)(参数列表);
例如:1
@property(nonatomic, copy) void (^callback)(int errorCode);
如果使用typedef来表示block,代码会更简洁,例如:
1 | typedef void (^WDCallbackBlock)(int errorCode); |
截获自动变量值
Block中可以直接使用声明在Block之前的变量和变量作用范围内的变量(如类成员变量和全局变量)
1 | int i = 0; //普通变量, Block中不能修改其数值. |
可以先想想Block中的log都会输出什么,为什么.
Block实现原理
使用Clang工具
Clang是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编译器。源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。 (来自百度百科)
我们这里使用它主要是为了将Objective-C代码解析成c++代码,从而来了解Block的实现原理, 因此只用到了最简单的一些功能. 转换的命令是:
1 | clang -rewrite-objc xxxx.m |
Block的类型
从转换后的c++源码可以看出,每个Block其实也是一个Objective-C中的对象,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14struct __BlockTestObject__testBlockParam_block_impl_0 {
struct __block_impl impl;
struct __BlockTestObject__testBlockParam_block_desc_0* Desc;
NSMutableString *s2;
int j;
NSString *s1;
__Block_byref_i_0 *i; // by ref
__BlockTestObject__testBlockParam_block_impl_0(void *fp, struct __BlockTestObject__testBlockParam_block_desc_0 *desc, NSMutableString *_s2, int _j, NSString *_s1, __Block_byref_i_0 *_i, int flags=0) : s2(_s2), j(_j), s1(_s1), i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,每个Block结构体中都有__block_impl结构
1 | struct __block_impl { |
- _NSConcreteStackBlock:在栈上创建的Block对象
- _NSConcreteMallocBlock:在堆上创建的Block对象
- _NSConcreteGlobalBlock:全局数据区的Block对象
这三种block的区别网上已经有很详细的介绍,这里在总结一下:
NSConcreteGlobalBlock
没有用到任何的类成员变量,全局变量,外部变量的block.
NSConcreteStackBlock
除了NSConcreteGlobalBlock 都是 NSConcreteStackBlock
NSConcreteMallocBlock
配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。因为现在的XCode默认都是ARC,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上
- 调用Block的copy方法
- 将Block作为函数返回值时
- 将Block赋值给__strong修改的变量时
- 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
Block工作流程
有了clang工具我们可以更好的分析Block的工作原理,下面我逐行来剖析Block的执行流程.
首先看这段OC代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21- (void)testNonValBlock {
int i = 0;
__block int bi = 0;
NSString *s = @"初始化";
NSMutableString *ms = [@"初始化" mutableCopy];
void (^block2)(void) = ^{
//int i = 1;
NSLog(@"testNonValBlock log, ");
NSLog(@"i = %d ",i);
NSLog(@"bi = %d ",bi);
NSLog(@"s = %@ ",s);
//ms = [@"block 里修改" mutableCopy];
NSLog(@"ms = %@ ",ms);
};
bi = 9;
i = 10;
s = @"改动了";
ms = [@"改动了" mutableCopy];
[ms appendString:@"增加了"];
block2();
}
解释后的C++代码: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
52struct __BlockTestObject__testNonValBlock_block_impl_0 {
struct __block_impl impl;
struct __BlockTestObject__testNonValBlock_block_desc_0* Desc;
int i;
NSString *s;
NSMutableString *ms;
__Block_byref_bi_1 *bi; // by ref
__BlockTestObject__testNonValBlock_block_impl_0(void *fp, struct __BlockTestObject__testNonValBlock_block_desc_0 *desc, int _i, NSString *_s, NSMutableString *_ms, __Block_byref_bi_1 *_bi, int flags=0) : i(_i), s(_s), ms(_ms), bi(_bi->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __BlockTestObject__testNonValBlock_block_func_0(struct __BlockTestObject__testNonValBlock_block_impl_0 *__cself) {
__Block_byref_bi_1 *bi = __cself->bi; // bound by ref
int i = __cself->i; // bound by copy
NSString *s = __cself->s; // bound by copy
NSMutableString *ms = __cself->ms; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_19);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_20,i);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_21,(bi->__forwarding->bi));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_22,s);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_23,ms);
}
static void __BlockTestObject__testNonValBlock_block_copy_0(struct __BlockTestObject__testNonValBlock_block_impl_0*dst, struct __BlockTestObject__testNonValBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->bi, (void*)src->bi, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->s, (void*)src->s, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->ms, (void*)src->ms, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __BlockTestObject__testNonValBlock_block_dispose_0(struct __BlockTestObject__testNonValBlock_block_impl_0*src) {_Block_object_dispose((void*)src->bi, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->s, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->ms, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __BlockTestObject__testNonValBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __BlockTestObject__testNonValBlock_block_impl_0*, struct __BlockTestObject__testNonValBlock_block_impl_0*);
void (*dispose)(struct __BlockTestObject__testNonValBlock_block_impl_0*);
} __BlockTestObject__testNonValBlock_block_desc_0_DATA = { 0, sizeof(struct __BlockTestObject__testNonValBlock_block_impl_0), __BlockTestObject__testNonValBlock_block_copy_0, __BlockTestObject__testNonValBlock_block_dispose_0};
static void _I_BlockTestObject_testNonValBlock(BlockTestObject * self, SEL _cmd) {
int i = 0;
__attribute__((__blocks__(byref))) __Block_byref_bi_1 bi = {(void*)0,(__Block_byref_bi_1 *)&bi, 0, sizeof(__Block_byref_bi_1), 0};
NSString *s = (NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_17;
NSMutableString *ms = ((id (*)(id, SEL))(void *)objc_msgSend)((id)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_18, sel_registerName("mutableCopy"));
void (*block2)(void) = ((void (*)())&__BlockTestObject__testNonValBlock_block_impl_0((void *)__BlockTestObject__testNonValBlock_block_func_0, &__BlockTestObject__testNonValBlock_block_desc_0_DATA, i, s, ms, (__Block_byref_bi_1 *)&bi, 570425344));
(bi.__forwarding->bi) = 9;
i = 10;
s = (NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_24;
ms = ((id (*)(id, SEL))(void *)objc_msgSend)((id)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_25, sel_registerName("mutableCopy"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)ms, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_yw_nyt6dyyx2pbcfvvfsrfh2hsc0000gn_T_BlockTestObject_fd76db_mi_26);
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
再补上一些基本的数据结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14struct __Block_byref_bi_1 {
void *__isa;
__Block_byref_bi_1 *__forwarding;
int __flags;
int __size;
int bi;
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
BlockTestObjecttestNonValBlock_block_impl_0 这个结构体是OC代码中block2的内存数据结构.
BlockTestObjecttestNonValBlock_block_desc_0 这个结构体是针对于block2的帮助函数,用来执行block2的复制和释放的类.
BlockTestObjecttestNonValBlock_block_func_0 这是个函数定义,是block2的真正的执行函数.
_I_BlockTestObject_testNonValBlock 这个函数就是我们的- (void)testNonValBlock 的执行函数.
__Block_byref_bi_1 是引用计数的简单实现.
Block的循环引用
其实理解了Block也是一个Object,也是有引用计数的话,循环引用的问题这里就不在累述了.
Block应用
1.实现下面的函数,将Block的实现修改成 NSLog(@”Hello world”)
也就是说,再调完这个函数后调用block()时,并不调用原始实现,而是打印”Hello world”1
2
3void HookBlockToPrintHelloWorld(id block) {
}
思路:
因为所有的Block执行的时候都是调用了__block_impl 结构中的FuncPtr来执行, 因此我们只要将block参数中的FuncPtr指针换成打印HelloWorld的函数就好了.
1 | static struct __block_impl copyBlock; |
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实现函数.
首先替换函数的问题很好解决,在上面一题中已经处理了这个问题.
打印参数问题
OC中有NSMethodSignature类,可以获取每个函数的函数签名.1
2
3
4
5
6
7
8const char *_Block_signature(void *);
const char *signature = _Block_signature((__bridge void *)block);
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:signature];
for (NSInteger i = 1;i<methodSignature.numberOfArguments;i++) {
const char *s = [methodSignature getArgumentTypeAtIndex:i];
NSString *str = [NSString stringWithUTF8String:s];
NSLog(@"param %d is %@", i, str);
}
调用原始实现.
这里的问题在于Block的实现函数的类型是未知且不是固定的, OC中无法显式用代码来描述未知参数的函数的调用.
但随着我们深入了解函数调用的原理之后,还是有办法来实现这个需求的.
与上一题不同的地方是这次我们保存原始Block的实现函数, 我在这里的处理方法是将Block对象做个拷贝. 一开始以为Block的copy方法是个深拷贝的行为, 然而并不是,于是我们只能将Block的数据结构展开以后做个内存拷贝,代码如下:
1 | struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block; |
然后将用户声明的Block实现函数指针替换成我们自己的函数, 为了适应不同参数的函数类型,我们只能把用来hook的函数定义为可变参数的函数.
1 | void myHookBlock(struct __block_impl *impl, ...) { |
上面代码中paramArray是我们在上面替换前保存的原始的Block执行函数的参数列表的类型, 因为不知道类型就不知道压栈的时候的参数的字节长度.所以我们必须依赖每个参数的类型才能正确的解析调用时的参数值.
另外可以学习的就是对于可变参数的一些处理,主要就是下面的三个函数,
1 | va_list arg_ptr; //定义可变参数的对象 |
最后的核心点在于NSInvocation这个对象了,这个对象允许调用任意的函数,包括类的成员函数,具体可以google.
学习资料
Aspects是一个轻量级的面向切面编程的库。它能允许你在每一个类和每一个实例中存在的方法里面加入任何代码。
Aspect github
libffi用于高级语言之间的相互调用。由于函数指针,参数类型,参数个数,参数的值都可以在运行时指定,所以在脚本语言调用c里面用的比较多,比如python 的ctypes;也可以调用不同abi(应用程序二进制接口)编译的程序,这个了解的不多。
libffi github