1. block的分类
1. NSGlobalBlock
我们通常把内存分为五大区,堆区
,栈区
,全局静态区
,常量区
,代码区
,当我们定义一个普通的block
函数:
void (^block)(void) = ^{
};
NSLog(@"%@",block);
通过打印
2021-02-18 09:57:19.402854+0800 001---Block[2105:42395] <__NSGlobalBlock__: 0x10dc4c088>
得出,block
当没有捕获外界变量的时候是存放在全局静态区的。
2. NSMallocBlock
因为block
拥有捕获外部变量的能力,当block捕获到外部变量时,就会由__NSGlobalBlock__
变为__NSMallocBlock__
。
int a = 10;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
NSLog(@"%@",block);
输出:
2021-02-18 10:01:48.334872+0800 001---Block[2259:49362] <__NSMallocBlock__: 0x60000184e9a0>
3. NSStackBlock
在定义block
时,我们通常用copy
或者strong
去修饰,因为在copy
操作之前,block
存放在栈区
,通过copy
我们可以将block
拷贝到堆区
。
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@" %d",a);
};
NSLog(@"%@",block);
输出:
2021-02-18 10:04:48.424646+0800 001---Block[2309:51300] <__NSStackBlock__: 0x7ffeec37a458>
当block
没有捕获外界变量时,是存放在全局区的,当我们对block
强引用时,block
存放在堆区,进行弱引用时存放在栈区。
2. 解决block循环引用
typedef void(^KCBlock)();
@interface ViewController ()
@property (nonatomic, copy) KCBlock block;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 循环引用
self.name = @"jeffery";
self.block = ^(void){
NSLog(@"%@",self.name);
};
self.block();
}
1. __weak
由上述代码可知,self
持有了block
,block
里又持有了self
,因此造成了循环引用,导致self
无法释放。通常我们都会用__weak
修饰self,
让block
对self
进行了弱引用,这样打破了双方的循环引用。但是通常在我们的业务逻辑中远没有这么简单,我们可能在block
内进行了一些业务代码的逻辑处理,如果只是简单的进行弱引用,可能导致在释放的时候所持有的对象已没了,这样,self
的生命周期是无法得到保证的,所以我们会在里面再进行一次强持有,来保证其生命周期。
__weak typeof(self) weakSelf = self;
self.block = ^(void){
__strong __typeof(weakSelf)strongSelf = weakSelf; // 可以释放 when
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
2. 中介者模式
__block ViewController *vc = self;
self.block = ^(void){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
因为通过__block
修饰的对象可以被block
捕获, 并且具有修改的能力。我们把self
赋值给vc
时,vc
就具有了修改等能力。因此,在我们对vc
进行了强持有后通过手动将vc
置为nil
,当前block
也就没有对vc
有持有关系,也就打破了循环引用。
3. 传值
typedef void(^KCBlock)(ViewController *vc);
self.block = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block(self);
直接将self
以参数的形式传入block
,block
对self
没有了持有关系,也就打破了循环引用。
3. block是如何捕获外部变量的
3.1 block是什么
创建一个block.c
文件:
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("jeffery");
};
block();
return 0;
}
通过clang -rewrite-objc block.c -o block.cpp
命令,导出一份block.cpp
文件。
通过底层探究可以得出^{}
在底层为__main_block_impl_0
这样一个函数,因为^{}
没有命名,所以block
为匿名函数。然后再文件中全局搜索__main_block_impl_0
,发现__main_block_impl_0
是一个结构体,这也是block
可以用%@
打印出来的原因,也间接说明了block
本质也是一个对象。
我们看到
printf("jeffery");
在
__main_block_func_0
方法里被打印出来,在
block
的构造函数里
impl.FuncPtr = fp;
这一步进行了赋值,也就是函数被当成参数传了进来,这样就可以在需要的地方进行调用。
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
去掉强转类型得到block->FuncPtr(block)
,这里是block()
的底层实现,这也是block必须进行调用的原因。
3.2 自动捕获外界变量
#include "stdio.h"
int main(){
int age = 18;
void(^block)(void) = ^{
printf("jeffery = %d",age);
};
block();
return 0;
}
我们对之前的block.c
进行了小修改,依然通过clang
命令编译出block.cpp
文件。
通过编译发现,
block
在编译期间就在内部生成了相应的变量
int age
,此时
isa
赋值为
&_NSConcreteStackBlock
,而我们之前通过打印出来的为
NSMallocBlock
,可以得出,当
block
捕获到外界变量时,在编译期存在于栈区,运行时存在于堆区。
我们在
block
调用代码里看到有一步赋值操作:
int age = __cself->age;
,很显然,这里的
age
与我们自己在
main
函数里定义的不是同一个变量。假如,我们在
block
进行了
age
的一些赋值操作的话,肯定会编译失败。这里的
age
只具备只读的功能。
__block
在int age = 18;
前使用__block
修饰一下,然后重新进行clang
编译。
通过编译发现,使用
__block
修饰的
age
不是
int
类型,而是为
__Block_byref_age_0
类型的指针。全局搜索发现
__Block_byref_age_0
为结构体类型。
main
函数里也多了一步结构体的赋值。
我们在代码里进行
age++
操作,再进行
clang
编译。
通过底层,我们可以看到这里是进行了指针的拷贝,因此这里的
age
与外界是同一个地址,然后我们就可以进行
age++
的操作。
总结:
通过__block
修饰的变量在底层编译时,会生成相应的结构体,保存了原来的指针和值,然后把指针的地址传给了block
进行捕获。
4. block源码解析
在源码中打开Block_private.h
文件,可以看到block
在源码中的形式为Block_layout
的结构体。
isa
:指向当前的block
类型,通过isa
判断是栈区,堆区还是全局区;
flag
:标志位;
因为Block_descriptor_1
是存在于block
中,而Block_descriptor_2
与Block_descriptor_3
是可选参数,我们通过flag
这个标志位判断当前是Block_descriptor_2
还是Block_descriptor_3
。打开runtime.cpp:
通过上述代码可以看到,
Block_descriptor_2
与
Block_descriptor_3
都是
flag&标志位
来判断是否存在,如果不存在,直接返回NULL,如果存在,则通过内存平移得到当前的
Block_descriptor_2
或
Block_descriptor_3
。
4.1 block_copy
首先对传入进来的
block
进行强转,然后通过标志位
flag
进行是否正在进行释放的判断,如果是则进行一些处理然后返回,然后判断是否为
全局block
,如果是
全局block
则没有必要进行拷贝,直接返回。因为在编译期间
block
不可能存放在堆区,所以只能是
栈区block
。然后便是对栈区block进行拷贝操作。首先申请开辟内存,然后进行
memmove内存拷贝
,然后判断是否将
invoke
也拷贝进来,然后再进行一些标志位的处理,将
isa
指向
堆区block
,处理完成将结果返回。
3. block 捕获外界变量过程
在底层进行clang的时候,我们看到__main_block_copy_0__main_block_copy_0
函数和__main_block_dispose_0
分别调用了_Block_object_assign
函数和_Block_object_dispose
,并在里面引用了使用__block
修饰的外部变量,这不禁让我记起了在探究block
时的Block_descriptor_2
的结构,当我们使用block_copy
的时候使用的是Block_descriptor_2
,所以这2个函数的调用应该是来自于Block_descriptor_2
。
因为我们常用普遍的大部分是
BLOCK_FIELD_IS_OBJECT(普通的对象)
和
BLOCK_FIELD_IS_BYREF(被block修饰的变量)
,所以主要研究这2个。
通过在
clang
我们将参数进行比较,第一二个参数对应的是我们拿到的外部变量。当是普通的对象类型时,先进行
_Block_retain_object
函数调用:
通过源码发现里面什么也没做,然后交给系统ARC进行持有,将当前对象进行指针拷贝,这样就有了2个地址指向了同一片内存空间,这样使得这片内存空间无法正常释放。
static struct Block_byref *_Block_byref_copy(const void *arg) {
// Block_byref 结构体
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// 问题 - block 内部 持有的 Block_byref 所持有的对象 是不是同一个
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));//内存偏移得到Block_byre_3
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
当是__block
修饰的对象时,进行了_Block_byref_copy
函数调用。在clang
的时候,我们已经知道被__block
修饰的对象在底层都会被编译成Block_byref
结构体。所以当外部变量传进来时,进行保存得到src
。然后定义一个Block_byref
的结构体copy
进行内存申请,变量赋值。经过clang
可知,在Block_byref
中,forwarding
指针对应的是外界变量的地址,在这里进行copy
和src
的赋值,让外部变量和block
内部变量持有的对象相等了。这也是经过__block
修饰的是同一片内存空间的原因。
接下来继续判断是否含有
copy
操作,如果有,进行拷贝操作,再调用
byref_keep
函数。
注:在这里因为外部变量是__block修饰的int类型,所以只进行了block本身的拷贝和Block_byref的拷贝,如果外部变量是__block修饰的对象类型时,还要对对象再进行一次拷贝,放到block本身的内存中。
当前类型如果是普通的类型,则调用系统ARC的释放,如果当前类型是
__block
修饰的对象时,调用
_Block_byref_release
函数进行释放。