iOS底层原理探索-Block本质(一)
首先,在学习之前,增加一些动力。经常在面试中,会被问及到这些问题:
block的本质是什么?
__block的作用是什么?原理是什么?有哪些使用注意点?
我们知道block在使用的时候,一般用copy修饰,用copy修饰发生了什么?具体过程是怎样的?
带着这些疑问,我们开始今天的学习。
block的数据结构长什么样?
首先,我们写一个简单的block,以及block的调用:
int age = 10;
void(^block)(int, int) = ^(int a, int b){
NSLog(@"调用该block----%d", age);
};
block(100, 100);
通过clang编译指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
将main.m文件转换为底层代码,通过查找可以看到上面代码转换为底层代码的相关代码:
int age = 10;
void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);
可以看到,block最后被转换为__main_block_impl_0类型
__main_block_impl_0类型是个什么样的结构存在的呢?
通过查看定义能够知道,__main_block_impl_0
的定义为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//函数调用的外部参数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
其中,__main_block_impl_0里面第一个类型__block_impl和第二个类型__main_block_desc_0的定义分别为:
struct __block_impl {
void *isa;//isa指针
int Flags;
int Reserved;
void *FuncPtr;//函数地址
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
首先,我们看到__main_block_func_0是一个函数;
main函数中__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)将参数__main_block_func_0传到了block里面,赋值给__main_block_impl_0里面的fp,然后做了 impl.FuncPtr = fp。
最后在执行block的时候,是执行的block->FuncPtr,就调用到了impl.FuncPtr,也就是fp,也就是__main_block_func_0
block内部直观表示大致如下:
总结:
- block是一个具有isa指针的oc对象
- block是封装了函数以及函数调用环境的OC对象
封装的函数是指block{}内部的代码,被转换成一个函数__main_block_func_0,并将函数地址封装在了__main_block_impl_0(block类型)内部的impl.FuncPtr
函数调用环境是指,函数调用的时候需要的参数,从图中可以看出,函数需要的变量age,已经被封装在了__main_block_impl_0里面。
接下来,我们分析下block转换为底层源码的代码
int age = 10;
void(block)(int, int) = ((void ()(int, int))&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA, age));
上句代码是Block的定义
一般小括号为强制转换,为了方便观察,可以将小括号以及小括号里面的内容删掉。
简化为:
void(block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age));
等号后面,是一个函数,函数名为__main_block_impl_0,函数有三个参数。并且获取函数地址后赋值给block对象。也就是block是一个指针变量。其内部存放的是__main_block_impl_0类型的地址。
而查看__main_block_impl_0的定义
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//函数调用的外部参数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;//block的类型是_NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0
函数与结构体名一样,该函数没有写返回值,但其实是返回结构体本身,该函数称为构造函数。
那么,block其实指向的是__main_block_impl_0结构体的地址。
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
sizeof(struct __main_block_impl_0):__main_block_impl_0即block的大小
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);
上句代码为block的执行,简化后的结果为:
(block->FuncPtr)(block, 100, 100);
利用block找到FuncPtr函数,进行调用。
看一下(block->FuncPtr)(block, 100, 100);
block即__main_block_impl_0类型,里面并没有FuncPtr函数
__main_block_impl_0里面的__block_impl类型里面才有FuncPtr函数,怎么block直接就调用了FuncPtr函数呢?
这是因为,里面有个强制转换操作,将block强制转换为__block_impl *类型,这样,就可以直接访问__block_impl里面的FuncPtr函数,即__block_impl->FuncPtr。
那,为什么这个可以将__main_block_impl_0强制转换为__block_impl类型呢?
这是因为,结构体__block_impl类型是__main_block_impl_0类型的第一个成员,那么__main_block_impl_0类型的内存地址跟__block_impl类型的内存地址是一样的,因此,可以强制转换。
从另一个角度去分析,__main_block_impl_0里面的__block_impl是一个结构体,而不是指针,相当于直接把__block_impl类型的内容放入__main_block_impl_0之中,也就相当于可以直接进行__main_block_impl_0->FuncPtr访问。
block捕获机制
block内部访问局部变量
来段简单的代码:
int age = 10;
void(^block)(void) = ^{
NSLog(@"调用该block----%d", age);
};
age = 20;
block();
很容易,我们知道最后的运行结果是:
调用该block----10
那么,是怎样一个原理呢?
同样,我们通过clang命令,将代码转换为底层代码:
int age = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
可以看到,block将age作为参数,传到__main_block_impl_0里面。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在__main_block_impl_0里面,block自己定义了一个同名变量age。
并通过age(_age)将_age的值赋值给age,即
age(_age) 等价于 age = _age;
执行
age = 20;
只是将int age = 10变为int age = 20,并没有改变block里面age的值
执行
block();
调用block实现,就调用了__main_block_impl_0里面的FuncPtr函数,而FuncPtr函数里面已经封装了age
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_65a3fe_mi_0, age);
}
该age是__cself->age,即block内部的age。而block内部的age=10。
因此,打印出来的age=10;也就是常说的值传递。
为什么值传递的值不可以赋值或者修改呢?
这个我们留到下一小节进行讲述
block内部访问static修饰的局部变量
如果用static修饰,会是怎么样呢?
int age = 10;
static int height = 170;
void(^block)(void) = ^{
NSLog(@"调用该block----%d, %d", age, height);
};
age = 20;
height = 180;
block();
结果:调用该block----10, 180
int age = 10;
static int height = 170;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 180;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
从上面可以看出,age传的是值,height是传的指针
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//新建同名变量age
int *height;//新建同名变量height,但是此height是指针变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0内部,新建的age是int,而height是指针int *。
执行height = 180;
执行block();
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_48f888_mi_0, age, (*height));
}
调用的时候,age是值传递,height是指针
由于height传的值是指针,*height已经修改为180,因此,block内部的height也被修改,因此最后打印出来的height是180
block内部访问全局变量
如果是全局变量或者static修饰的全局变量,运行结果又有什么不一样呢?
int age = 10;
static int height = 170;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"调用该block----%d, %d", age, height);
};
age = 20;
height = 190;
block();
}
return 0;
}
运行结果:调用该block----20, 190
转换为底层代码后:
从底层源码可以看出,__main_block_impl_0内部没有新定义age,或者height。
说明block内部并没有捕获外部的全局变量。
最后调用函数,打印的age和height是全局变量。
通过以上代码,我们可以发现,block访问外部变量,有一个变量捕获机制(capture)捕获机制
怎么理解捕获呢?
在block内部专门新建一个变量,用来存储外部的值,称为捕获。
通过以上例子,可以得出:
block访问外部变量总结:
为什么使用auto修饰的变量,block捕获的值,而使用static修饰的局部变量,block捕获的是指针呢?
这是因为,auto修饰的变量,随时可能被销毁,因此,需要及时把值捕获进去。
而static修饰的变量,在程序整个生命周期都存在,所以,可以对变量进行修改,因此只需要捕获指针即可。
全局变量,存储在静态全局区,整个程序的生命周期都存在,因此,不需要捕获
- (void)test
{
void(^block)(void) = ^{
NSLog(@"调用该block----%@", self);
};
block();
}
问:该block里面的self,是否会被捕获?
同样,使用clang转换为底层代码,可以看到block定义:
struct __YZPerson__test_block_impl_0 {
struct __block_impl impl;
struct __YZPerson__test_block_desc_0* Desc;
YZPerson *self;
__YZPerson__test_block_impl_0(void *fp, struct __YZPerson__test_block_desc_0 *desc, YZPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,捕获到了self。
问:为什么会捕获self呢?
- (void)test
{
void(^block)(void) = ^{
NSLog(@"调用该block----%@", self);
};
block();
}
最后转化为:
static void _I_YZPerson_test(YZPerson * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__YZPerson__test_block_impl_0((void *)__YZPerson__test_block_func_0, &__YZPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
可以看出,在test函数里面,其实是有两个隐式变量:self和SEL类型的_cmd。
self是以参数传进去的,因此,self属于局部变量,需要进行捕获。
既然这样的话,那么:
- (void)test
{
void(^block)(void) = ^{
NSLog(@"调用该block----%@", _name);
};
block();
}
_name又是如何存在的呢?捕获还是不捕获?捕获的话是直接捕获还是怎样的呢?
不多说,咱还是直接看底层代码:
从图片中可以看到,block并没有捕获name,而是通过捕获的self,访问的_name。
其实可以理解,因为name属于YZPerson里面的一个属性,_name是YZPerson里面的一个成员变量,_name其实是self->_name,因此,是通过捕获self,访问_name成员变量的。
这样说明了,在block内部通过访问成员变量,就相当于里面引用了self,因此,还是需要留意循环引用的问题。
block类型
block有三种类型:
__NSGlobalBlock__
__NSStackBlock__
__NSMallocBlock__
block的三种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承NSBlock类型,再往上是继承NSObject类型。
举个例子:
void(^block)(void) = ^{
NSLog(@"调用该block----");
};
NSLog(@"1-%@", [block class]);
NSLog(@"2-%@", [[block class] superclass]);
NSLog(@"3-%@", [[[block class] superclass] superclass]);
NSLog(@"4-%@", [[[[block class] superclass] superclass] superclass]);
NSLog(@"5-%@", [[[[[block class] superclass] superclass] superclass] superclass]);
NSLog(@"6-%@", [[[[[[block class] superclass] superclass] superclass] superclass] superclass]);
运行结果:
2020-03-25 11:08:14.326871+0800 block学习[53532:3297757] 1-__NSGlobalBlock__
2020-03-25 11:08:14.327226+0800 block学习[53532:3297757] 2-__NSGlobalBlock
2020-03-25 11:08:14.327276+0800 block学习[53532:3297757] 3-NSBlock
2020-03-25 11:08:14.327317+0800 block学习[53532:3297757] 4-NSObject
2020-03-25 11:08:14.327349+0800 block学习[53532:3297757] 5-(null)
2020-03-25 11:08:14.327377+0800 block学习[53532:3297757] 6-(null)
可以看出,该block的类型是 NSGlobalBlock,其继承关系是:NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject
至于为什么NSObject的superclass是nil可以参考iOS中对象的本质。
问:那,三种类型具体什么时候是哪种类型呢?
先上个总结图:
具体的实验结果可以参考iOS之Block基本使用
其中,在ARC下,你会发现,
int a = 3;//局部变量
void(^block)(void) = ^{
NSLog(@"调用了block, a = %d", a);
};
NSLog(@"%@", block);
结果:<__NSMallocBlock__: 0x28343ea60>
按照之前的总结图,block的存储类型不应该是NSStackBlock吗?怎么打印出来的却是NSMallocBlock?
这是因为,在ARC中,系统以及自动帮我们做了copy操作。从而将本应该是NSStackBlock经过copy操作后,变为NSMallocBlock。
每一种类型的block调用copy后的结果如下:
为什么ARC需要帮我们把本存储在NSStackBlock的block经过copy操作,转移存储在NSMallocBlock上呢?
一个在MRC下的例子:
void(^block)(void);
void test()
{
int age = 10;
block = ^{
NSLog(@"调用该block----%d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
结果:
调用该block-----272632728
可以看到,age的值不是10,而是一串莫名其妙的数字
这是因为,block引用了auto变量,block类型是NSStackBlock。
在test()括号执行完毕后,block其实已经被释放了,再次调用block,里面的age就不是10了。
因此,我们需要将存在栈上的block通过copy操作,转移存储在堆上。将生命周期交给程序员自己控制。
总结:
在ARC环境下,编译器会根据以下情况自动将栈上的block复制到堆上:
block作为函数返回值时
将block赋值给strong指针时(即block有强指针引用)
block作为Cocoa API中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时
三个block类型具体存储在哪一个区域
block与copy、retain、release操作
对不同类型的block,调用其retainCount,观看其有何不同点:
以下是验证程序:
NSGlobalBlock
- (void)viewDidLoad {
[super viewDidLoad];
//没有访问auto变量,存储在NSGlobalBlock
void(^block)(void) = ^{
};
NSLog(@"%@", block);
[block retain];
[block retain];
[block retain];
NSLog(@"[block retainCount] = %d", [block retainCount]);
NSLog(@"%@", block);
}
打印结果:
2020-07-15 18:42:26.133369+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
2020-07-15 18:42:26.133506+0800 block2[9804:1002328] [block retainCount] = 1
2020-07-15 18:42:26.133586+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
NSStackBlock
- (void)viewDidLoad {
[super viewDidLoad];
int a = 3;//局部变量
//访问auto变量,存储在NSStackBlock
void(^block)(void) = ^{
NSLog(@"调用了block, a = %d", a);
};
NSLog(@"%@", block);
[block retain];
[block retain];
[block retain];
NSLog(@"[block retainCount] = %d", [block retainCount]);
NSLog(@"%@", block);
}
打印结果:
2020-07-15 18:43:43.936774+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
2020-07-15 18:43:43.936910+0800 block2[9825:1003381] [block retainCount] = 1
2020-07-15 18:43:43.936996+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
NSMallocBlock
- (void)viewDidLoad {
[super viewDidLoad];
int a = 3;//局部变量
//访问auto变量,存储在NSStackBlock
void(^block)(void) = [^{
NSLog(@"调用了block, a = %d", a);
} copy];//调用copy,存储在NSMallocBlock
NSLog(@"%@", block);
[block retain];
[block retain];
[block retain];
NSLog(@"[block retainCount] = %d", [block retainCount]);
NSLog(@"%@", block);
}
打印结果:
2020-07-15 18:44:48.964249+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
2020-07-15 18:44:48.964366+0800 block2[9842:1004304] [block retainCount] = 1
2020-07-15 18:44:48.964456+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
block与copy、retain、release操作的总结:
不同于NSObjec的copy、retain、release操作:
Block_copy与copy等效,Block_release与release等效;
对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount 始终是1;
NSGlobalBlock:retain、release、copy操作都无效;
NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。
支持copy,copy之后生成新的NSMallocBlock类型对象。
NSMallocBlock:支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。
copy之后不会生成新的对象,只是增加了一次引用,类似retain;
尽量不要对Block使用retain操作。