本文将围绕以下几个问题分析block:
-
block是不是函数指针?如果不是它们有什么区别?
-
block修改局部变量为什么必须要加
__block
修饰? -
block为什么会产生循环引用?应该怎么避免?
要完全搞清楚这几个问题,首先我们需要搞清block的本质是什么?
结论:在iOS中block本质上就是一个oc对象,内部同其他OC对象一样有一个isa指针。block是一个将函数定义、实现、调用封装在一起的OC对象;
第一步:先写一个简单的测试block
- (void)test {
void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b){
NSLog(@"block a = %ld, b = %ld",a, b);
};
block(10, 20);
}
用命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m
将OC文件转换成c++代码查看OC内部实现结构
static void _I_ViewController_test(ViewController * self, SEL _cmd) {
void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA));
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
}
从上述代码可以看到,block的定义被转换成了
void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA));
实际上是把函数__ViewController__test_block_impl_0的函数地址赋值给了block,
应该在这段代码之上我们可以看到__ViewController__test_block_impl_0其实是一个结构体,结构如下:
struct __ViewController__test_block_impl_0 {
struct __block_impl impl;
struct __ViewController__test_block_desc_0* Desc;
__ViewController__test_block_impl_0(void *fp, struct __ViewController__test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
该结构体内部包含了两个变量impl
、Desc
和一个同名构造函数__ViewController__test_block_impl_0,全局搜索__block_impl
找到其结构如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
-
isa:类型指针,这里代表block的类型
-
Flags:这里从构造函数的调用看到它有一个默认值0,并未对其赋值,也许在更深层有其它的用处
-
Reserved:应该同Flags差不多在底层有使用吧
-
FuncPtr:函数指针,这里指向__ViewController__test_block_func_0函数
到这里足以证明block就是一个OC对象,同样有这一个指向它类型的isa指针;结构体__ViewController__test_block_impl_0的下方应该就能看到另一个变量
Desc
其结构如下:static struct __ViewController__test_block_desc_0 { size_t reserved; size_t Block_size; } __ViewController__test_block_desc_0_DATA = { 0, sizeof(struct __ViewController__test_block_impl_0)};
__ViewController__test_block_desc_0
包含了两个变量,并在定义时创建了一个__ViewController__test_block_desc_0_DATA结构体 -
reserved:字面意思
保留
,应也是一个内部使用字段,这里能看到为其赋值0 -
Block_size:sizeof(),这里代表结构体__ViewController__test_block_impl_0的内存占用大小
void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA));
回看我们的block定义,__ViewController__test_block_impl_0调用时还传入了一个__ViewController__test_block_func_0函数
static void __ViewController__test_block_func_0(struct __ViewController__test_block_impl_0 *__cself, NSInteger a, NSInteger b) {
// NSLog(@"block a = %ld, b = %ld",a, b); OC block 内部代码
NSLog((NSString *)&__NSConstantStringImpl__var_folders_dm_tq28fyls0cq42nl6fdhyqzd40000gn_T_ViewController_939d4d_mi_0,a, b);
}
从函数结构就能看出该函数其实就是block{}里面的实现,因此我们得出block就是OC对象,内部封装了c函数定义、实现、和函数调用;
1.那么block是不是函数指针?如果不是它们有什么区别?
综上所述,block不是函数指针。函数指针是指向一段预先定义好的可执行代码,且该函数地址是在编译时就已经确定好的,函数内只能访问全局变量。block是OC对象,可以在运行时自由创建,不同作用域内创建的block对象的生命周期也不一样,通常动态创建的block都存储在栈上,可以调用[block copy]
将其拷贝到堆上延长生命周期,另外block对局部变量拥有读的权限,对使用__block修饰的局部变量拥有读写权限
2.block修改局部变量为什么必须要加__block
修饰?
- (void)test {
int j = 10;
int g = 10;
static int s = 10;
__block int i = 10;
void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b){
i++;
s++;
self;
// j++; 报错:Variable is not assignable (missing __block type specifier)
NSLog(@"block a = %ld, b = %ld",a, b);
NSLog(@"block i = %d, j = %d",i, j);
};
block(10, 20);
}
在原有test
方法内新增成员变量j、g,static变量s,__block变量i
,再次执行命令转换为c++,结果如下:
static void _I_ViewController_test(ViewController * self, SEL _cmd) {
int j = 10;
int g = 10;
static int s = 10;
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 10};
void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA, &s, self, j, g, (__Block_byref_i_0 *)&i, 570425344));
((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
}
可以看到前三个变量并没有什么变化,只有__block 变量i
被转换成了__Block_byref_i_0
结构体:
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
其__isa=(void*)0
,__forwarding=(__Block_byref_i_0 *)&i
指向i自身地址的指针,__flags=0
,__size=sizeof(__Block_byref_i_0)
,i=10
为原来i的值,修改后block的实现变成如下:
struct __ViewController__test_block_impl_0 {
struct __block_impl impl;
struct __ViewController__test_block_desc_0* Desc;
int *s;
ViewController *self;
int j;
int g;
__Block_byref_i_0 *i; // by ref
__ViewController__test_block_impl_0(void *fp, struct __ViewController__test_block_desc_0 *desc, int *_s, ViewController *_self, int _j, int _g, __Block_byref_i_0 *_i, int flags=0) : s(_s), self(_self), j(_j), g(_g), i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
仔细看会发现在函数__ViewController__test_block_impl_0()
后面还有这么一截: s(_s), self(_self), j(_j), g(_g), i(_i->__forwarding)
这一截都是对结构体__ViewController__test_block_impl_0内的自定义变量进行复制s(_s)
等价于s = _s
其他同理,结合改变后的block定义:
void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA, &s, self, j, g, (__Block_byref_i_0 *)&i, 570425344));
看调用__ViewController__test_block_impl_0
函数时传入的参数((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA, &s, self, j, g, (__Block_byref_i_0 *)&i, 570425344)
前两个同修改前一样前面有介绍,从第三个开始分别为:静态变量s的地址、当前类对象self、j和g传入的都是int类型数值、(__block变量转换后)结构体i的地址,最后那个数字对应的是flags
那么,block修改局部变量为什么必须要加__block
修饰?
因为,使用__block修饰后的变量在block内部会创建一个相应的结构体,该结构体内保留了变量的值和地址,block内部还为结构体定义了一个指针变量,内部对原来变量的读写都是通过这个结构体指针来实现的,所以就完成了对__block
变量的修改;更多细节
3.block为什么会产生循环引用?应该怎么避免?
通过以上内容分析,我们知道block内会对外部变量、对象产生一个引用关系,OC对象在没有指定强弱引用的情况下,默认是强引用,那么一个对象如果间接或直接拥有block,block内部又拥有该对象那么就会产生循环引用
如图1中object----(strong)---->object1----(strong)---->block----(strong)---->object就是一个间接性引用产生的循环引用
图1解决图1中问题其实只需将三条箭头中任意一条改用弱引用就能打破循环保留问题,但在实际开发过程中我们调用block一般都不是及时性的,所以如果object----(weak)---->object1或者object1----(weak)---->block当我们在调用block时可能对象已经被释放,代码运行会违背我们预期效果,即解决block循环引用问题,我们推崇将block-->object外部改成弱引用,block内部在将弱引用对象转强引用避免block内部访问还没结束object就被释放问题,如图2:
图2参考文章:
iOS Block原理探究以及循环引用的问题
网友评论