我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
前言
Block是我们项目经常用到的地方,今天我们就来学习一下关于 Block 的原理,__block 的作用,Block 的循环引用问题。让我们更深刻的了解下 Block 的本质。
Block的类型
全局Block
__NSGlobalBlock__ :全局 Block,位于全局区,且在 block 内部不使用外部变量,或者只使用静态变量或者全局变量。
static int a = 20;
void (^gloalblock)(void) = ^{
NSLog(@"gloalblock: - %d",a);
};
gloalblock();
NSLog(@"%@",gloalblock);
打印如下;
<__NSGlobalBlock__: 0x10bf850b8>
堆Block
__NSMallocBlock__ :堆区 Block ,在 block 内部使用外部变量或者 OC 属性,并且赋值给强引用或者 copy 修饰的变量。
int b = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"mallocBlock - %d",b);
};
mallocBlock();
NSLog(@"%@", mallocBlock);
打印如下:
<__NSMallocBlock__: 0x600000c50210>
栈Block
__NSStackBlock__ :- 与 MallocBlock 一样,可以在内部使用局部变量或者 OC 属性,但是不能赋值给强引用或者 Copy 修饰的变量。
void (^__weak stackBlock)(void) = ^{
NSLog(@"stackBlock----%d",b);
};
stackBlock();
NSLog(@"%@", stackBlock);
打印如下:
<__NSStackBlock__: 0x7ff7b3f7a260>
这里只是演示而已,一般我们不这么写。像上面的例子堆Block,在 block 没有 copy 之前,是栈 Block,拷贝之后就变成了堆 Block。
Block底层分析
我们先在main.m里面创建1个block。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^ block)(void) = ^{
NSLog(@"----%d",a);
};
block();
}
return 0;
}
然后在终端用 clang 生成 main.app 文件。使用方法:clang -rewrite-objc main.m。
然后我们打开main.cpp。
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
void (* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
我们可以发现block 底层会被编译成一个结构体类型 __main_block_impl_0。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
其中__block_impl 的结构如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
先分析这一段:
void (* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
从这里看到是在底层block的类型__main_block_impl_0结构体,通过其同名构造函数创建,第一个传入的是__main_block_func_0,用fp表示,传入到impl.FuncPtr = fp中,这个是block的函数代码块。
第二个传的是__main_block_desc_0_DATA,存放在Desc = desc中,是描述信息的函数地址;
第三个直接保存a,可以看到是值拷贝。
那这里的重点就是:block捕获外界变量时,在内部会自动生成同一个属性来保存。如果是静态局部变量,则是指针拷贝,如果是全局变量,则直接访问。
__block
我们先改一下代码。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
void (^ block)(void) = ^{
a++;
NSLog(@"----%d",a);
};
block();
}
return 0;
}
继续生成main.cpp文件。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到是,这么我们存的是结构体指针__Block_byref_a_0。,a(_a->__forwarding)是&a,也就是说对象 a 的地址,和结构体指针__Block_byref_a_0是一样的。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_sk_t102mr_53p70tshnj46v9k880000gp_T_main_760d82_mi_0,(a->__forwarding->a));
}
这里是通过指针拷贝,此时我们的对象a和__cself->a指向的是同一片空间区域。这时候我们进行(a->__forwarding->a)++操作,相当于外面的对象a也进行了++操作。
这里的重点就是:block捕获外界变量时,将变量生成的结构体对象的指针地址传递给block,block内部就可以对外界变量进行操作。
Block的3层copy
只有 __block 修饰的对象,才有3层拷贝。
-
通过
_Block_copy,将栈区的block拷贝一份到堆区。 -
__block修饰的对象,通过_Block_byref_copy方法,把对象拷贝为Block_byref结构体类型。 -
通过
_Block_object_assign方法,对__block修饰的当前变量的拷贝。
循环引用
循环引用:对象之间互相持有,形成一个闭环,导致谁也无法正确释放。
这里 self 持有 block ,block 又持有着 self ,互相强引用,所以提示循环引用。
__weakself弱引用
- (void)test {
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakself = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
self.block = ^(void){
weakself.str = @"你好";
NSLog(@"%@",weakself.str);
};
self.block();
}
打印结果:
分析:进来此页面后,先打印 2 个 6 ,证明了 weakself 并没有对 self 的引用计数增加 1 , self 持有 block ,block 持有的是 weakself 指针,weakself 并没有强引用 self ,一旦 self 释放后,block 也就会释放了。
换一个例子:
- (void)test {
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakself = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
self.block = ^(void){
weakself.str = @"你好";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakself.str);
});
NSLog(@"%@",weakself.str);
};
self.block();
}
直接看打印结果吧:
和你们想的是一样吗,进来页面后,又马上离开,这时候dispatch_after 还没执行完,这时候调用 weakself , weakself 已经被释放了,所以打印为 null。
那我们就添加一个强引用吧。
- (void)test {
__weak typeof(self) weakself = self;
self.str = @"你好";
self.block = ^(void){
__strong typeof(self) strongself = weakself;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongself.str);
});
NSLog(@"%@",strongself.str);
};
self.block();
}
打印一下结果:
可以看到,我们退出后, 2 秒后依然正常输出,再 dellac 。这是因为我们 strongself 是强引用 self ,会对 self 引用计数 +1 , self 就不能正常释放,等走完 dispatch_after 后,因为 strongself 是局部变量,走完后就会被回收,这时候 self 就可以正常释放了。
__block释放
我们可以添加__block来操作。用 __block 修饰,然后 block 调用完后,置为 nil ,也可以打破循环引用。
- (void)test
{
__block BViewController*vc = self;
self.block = ^(void){
NSLog(@"%@",vc.str);
vc = nil;
};
self.block();
vc 持有self, self 持有 block ,block 持有 vc ,这时候调用完 vc 置为空,就可以打破循环。所以self.block()是一定要执行的,不然 self 被 vc 持有,就释放不了 self 了。
最后
Block的 分析就到这里了,后面有时间,再写一下 weak,为什么 delegate 要用 weak 修饰呢,用 assign 修饰会怎样?
参考资料