Block的本质<一>
1.对象类型的auto变量
在第一篇文章中我们讲了在block中使用基本类型的自动变量的情况,现在我们研究一下在block中使用对象类型的自动变量又是什么情况。
首先看一段代码:
typedef void(^PDBlock)(void);
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block;
Person *person = [[Person alloc] init];
person.age = 10;
block = ^{
NSLog(@"%ld", (long)person.age);
};
block();
return 0;
}
}
这段代码执行的结果是10,完全没有问题。我们为了研究person对象的释放时机,在person.m文件中实现dealloc方法:
- (void)dealloc{
NSLog(@"--------dealloc");
}
然后我们把代码改一下,在大括号下面一行打断点:
运行代码,发现运行到断点处的时候,已经打印了
--------dealloc
,这说明这个时候person对象已经被释放了。这个很好理解,因为person对象是在大括号内声明的局部变量,它的生命周期仅限于这个大括号。
下面我们在大括号里加入一个block:
这个时候运行代码,发现代码运行到断点处,也就是出了大括号,还没有打印
--------dealloc
,这说明person对象出了大括号还没有被释放。为了探索其中的原因,我们把代码转化为c++源码看看,为了简洁,把代码简化如下:
typedef void(^PDBlock)(void);
int main(int argc, char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
PDBlock block = ^{
NSLog(@"%ld", (long)person.age);
};
}
return 0;
}
直接看_mian_block_imol_0这个结构:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这个结构中多了一个成员变量Person *person
,因为person是自动变量,所以这里捕获了自动变量person作为_main_block_impl_0结构体的成员变量。而且还要注意的是,由于是自动变量,所以在block外面,自动变量是什么类型,在结构体里面作为成员变量就是什么类型。person在结构体外面作为自动变量是指针类型,所以作为结构体里面的成员变量也是指针类型。
结构体里面有一个成员变量是person指针,这个person指针指向在外面创建的那个person对象。所以出了大括号以后,外面的person指针被销毁了,但是block内有一个person指针还指向原来的person对象,所以原来的person对象没有销毁。
下面我们在MRC下运行一下代码,对代码进行一下小修改:
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block;
{
Person *person = [[Person alloc] init];
person.age = 10;
block = ^{
NSLog(@"%ld", (long)person.age);
};
[person release];
}
}
return 0;
}
还是在这个大括号外面打断点,我们惊奇的发现代码运行到断点处时已经打印了--------dealloc
,这个时候唯一的不同就是在MRC环境下block是在栈空间,那这是不是就说明在栈空间的block对person对象没有像堆空间的block那样的强引用效果呢?我们把这个block进行copy操作查看一下结果。
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block;
{
Person *person = [[Person alloc] init];
person.age = 10;
block = [^{
NSLog(@"%ld", (long)person.age);
} copy];
[person release];
}
}
return 0;
}
这个时候我们发现在大括号外的断点处,并没有打印--------dealloc
,这说明这个时候person对象还没有被释放,也就是说此时堆区的block对person对象是有强引用作用的,栈空间的block对person对象没有强引用作用。
我们再切换回ARC,执行下面代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block;
{
Person *person = [[Person alloc] init];
person.age = 10;
__weak Person *weakPerson = person;
block = ^{
NSLog(@"%ld", (long)weakPerson.age);
};
}
}
return 0;
}
还是在大括号外面打断点,我们看到在断点处就已经打印了--------dealloc
,这说明在这里person对象就已经被销毁了。我们输入clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.00 main.m
将其转化为c++的代码查看一下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
查看_main_block_impl_0,我们发现,这里面多了一个成员变量Person *__weak weakPerson
,可以看到weakPerson这个成员变量带有weak属性吗,这和外面的自动变量weakPerson保持一致。由于这里成员变量是weak属性,所以对person对象是弱引用,也就不能改变person对象的生命周期。
总结 当block内部访问了对象类型的auto变量时
如果block是在栈上,将不会对auto变量产生强引用
如果block被拷贝到堆上
- 会调用block内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会根据auto变量的修饰符(__strong,_weak)做出相应的操作,形成强引用,弱引用。
如果block从堆上移除
- 会调用block内部的dispose函数
- dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的auto变量,类似于release。
2.__block修饰符
有时候我们想要在block内部修改自动变量的值,就像下面这样:
PDBlock block;
int age = 10;
block = ^{
age = 20;
NSLog(@"%d", age);
};
但是很快我们就会发现这样写报了编译错误,不能在block内修改自动变量的值。其实从c++的源码上我们也能明白,age是在main函数中声明的自动变量,而block内的代码块是_main_block_func_0这个函数,这两个函数互不相干,所以在_main_block_func_0函数内是不能修改在main函数中定义的自动变量的。换个角度思考这个问题,由于自动变量被block捕获是值传递,所以block只是捕获了age的值10,并把它赋值给自己的成员变量,使得结构体中有一个成员变量的值是10,要用的时候就把这个值读出来,能够修改结构体的成员变量的值,使之变成20,但是这并不会改变外面自动变量的值。
那么如果我们把局部变量改成用static修饰会怎么样呢?
PDBlock block;
static int age = 10;
block = ^{
age = 20;
NSLog(@"%d", age);
};
block();
编译没有问题,打印结果:
20
得到了我们所期望的结果。
那么为什么使用static修饰的局部变量就可以在block内修改值呢?
因为用static修饰的局部变量,被block捕获时是指针传递,也就是把存放这个值的地址给传进去了,在结构体内会有一个指针类型的成员变量存放传经来的这个地址,如果要修改外面的局部变量的值,那么就可以通过地址值去改变其值。
但是使用static修饰符会改变变量的生命周期,使之一直存在内存中,有时候这并不是我们想要的,那么有没有其它办法使自动变量的值能够在block内被修改呢?这个时候就要用到__block修饰符了。
PDBlock block;
__block int age = 10;
block = ^{
age = 20;
NSLog(@"%d", age);
};
block();
执行代码,输出结果是20。那么__block修饰符的本质原理是什么呢?我们还是从c++的源码来寻找结果:
main函数:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
PDBlock block;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
化简一下:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
PDBlock block;
__Block_byref_age_0 age = {0,
&age,
0,
sizeof(__Block_byref_age_0),
10};
block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age, 570425344));
block)->FuncPtr(block);
}
return 0;
}
我们可以看到,__block int age = 10
这句赋值语句被转换成了:
__Block_byref_age_0 age = {0,
&age,
0,
sizeof(__Block_byref_age_0),
10};
把age转换成了一个结构体,我们看一下_Block_byref_age_0这个结构体的结构:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
通过赋值我们可以知道,&age被赋值给了自己的成员变量_forwarding这个指针,_size是结构体的大小,成员变量age存放的是自动变量的值。所以_forwarding这个指针是指向结构体自身的一个指针。总结起来就是age这个结构体的成员变量包括isa指针,指向自身的_forwarding指针,还有一个int类型的成员变量存放值。
我们再看一下_main_blcok_impl_0的结构:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这里面有一个结构体指针age,在其结构体函数中是用&age来初始化的,也就是用前面创建的age结构体的地址来初始化_main_blcok_impl_0的成员变量(age指针)。
我们再看一下_main_blcok_func_0的结构:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_db631a_mi_0, (age->__forwarding->age));
}
通过_cself->age访问自己的成员变量来获取结构体指针,然后age->_forwarding->age通过结构体指针访问成员变量来改变成员变量的值,最后在取值的时候也是通过age->_forwarding->age来取值。
3.__block内存管理
我们已经知道了在栈上的block不会对对象类型的局部变量产生引用,但是堆上的block会对对象类型的局部变量产生引用。那么这个过程是怎么样的呢?我们看一段代码:
PDBlock block;
NSObject *objc = [[NSObject alloc] init];
__weak NSObject *weakObjc = objc;
^{
NSLog(@"%@ %@", objc, weakObjc);
};
我们把它转化为c++的源码看看:
找到_main_block_desc_0这个结构体:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
可以看到,这个结构体对比之前有一些变化,以前成员变量只有reserved和Block_size这两个,但是这里又增加了两个函数作为成员变量,一个是copy,一个是dispose函数。并且我们从结构体的构造函数可以知道这个copy函数是由_main_block_copy_0这个函数初始化的,dispose是由_main_block_dispose_0这个函数初始化的。当block从栈区复制到堆区的时候会调用这里的copy函数,当block销毁的时候会调用dispose函数。我们往上查看一下这两个函数:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->objc, (void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->weakObjc, (void*)src->weakObjc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->weakObjc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
在_main_block_copy_0这个函数中主要是调用了_Block_object_assign这个函数,这个函数的作用就是对捕获的对象类型的自动变量产生引用。如果外面的对象类型的自动变量是用weak修饰,那么_Block_object_assign函数就会使block对这个对象产生弱引用,如果是strong类型的,那么就会产生强引用。
在_main_block_dispose_0这个函数中,主要是调用了_Block_object_dispose这个函数,释放对对象的强引用或弱引用。
上面是分析的对对象类型的auto变量的内存管理问题,我们知道,__block修饰的自动变量在本质上就转化为了结构体实例,也就是一个OC对象,既然这样,那么也必然存在内存管理的问题。
在block还未复制到堆上时,由__block修饰的自动变量产生的结构体实例也存在栈上,当block从栈上复制到堆上后,block结构体中指向自动变量产生的结构体实例的那个指针也随之被复制到了堆区,这样就产生了堆区的指针指向栈区的对象这个问题,这显然是不行的,所以就要将栈区的结构体实例赋值到堆区,同时,block对堆区的结构体实例进行强引用。
总结
当block在栈上时,并不会对__block变量产生强引用。
当block被copy到堆上时
- 会调用block内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会对__block变量形成强引用
当block从堆中移除时 - 会调用block内部的dispose函数
- dispose函数内部会调用_Block_object_dispose函数
- _Block_object_dispose函数会自动释放引用的__block变量
4._forwarding指针的作用
在2中我们了解到,读取和改变age的值都是通过age->_forwarding->age这样的,也就是先通过_main_block_impl_0这个结构体获取其成员变量age指针-指向结构体的指针,然后再通过age指针获取结构体的成员变量_forwarding指针,这个指针是指向结构体自身的,然后再通过这个指向自身的结构体指针访问自己的成员变量age,这个age成员变量里面真正存放着值。那么问题来了,这个_forwarding指针到底有什么用呢?
我们思考一个问题,当block还在栈区时,这时候包装age的结构体实例也是分配在栈区的,所以block这个结构体内部的age指针是指向栈区的结构体实例的,这时候通过age结构体指针直接访问结构体内部的age成员变量值和通过age->_forwarding->age来访问,结果都是一样的。
但是,当block从栈区复制到堆区时,封装age的结构体也会复制一份到堆区,这个时候其实block结构体内部age指针还是指向栈区的age结构体,那这样的话无论如何都无法取得堆区的age结构体。Apple显然想到了这个问题,在age结构体从栈区复制到堆区的过程中,内部将栈区的age结构体的_forwarding指针修改了,改为指向堆区的age结构体。这样一来,我们再通过block结构体访问age指针时,访问到了栈区的age结构体,然后通过栈区的age结构体的_forwarding指针去访问堆区的age结构体,也就是age->_forwarding->age这样一个过程。
5.__block修饰对象
前面分析了__block修饰基本类型,现在来分析一下__block修饰对象类型。我们看一段代码:
PDBlock block;
__block Person *person = [[Person alloc] init];
block = ^{
NSLog(@"%@", person);
};
这段代码是用__block修饰对象类型的简单的例子,我们看一下转化的源码:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
PDBlock block;
__attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
(void*)0,
(__Block_byref_person_0 *)&person,
33554432,
sizeof(__Block_byref_person_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
}
return 0;
}
我们可以看,这里对象类型的自动变量person也是被封装为了一个结构体,查看一下_Block_byref_person_0这个结构体的结构:
struct __Block_byref_person_0 {
void *__isa;
__Block_byref_person_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);//从栈上复制到堆上要进行的操作
void (*__Block_byref_id_object_dispose)(void*);//从栈上复制到堆上要进行的操作
Person *person;
};
这里和之前的结构不同的是最后一个成员变量是一个指针,这个指针是指向外面用alloc创建的person对象。同样,在block结构体中也有一个结构体指针,这个指针是指向_Block_byref_person_0这个结构体的,总结一下就是:
如此一来,当block从栈区复制到堆区的时候,会调用_main_block_copy_0来实现第一个箭头的引用,那么第二个箭头的引用是怎么实现的呢?我们发现,在_Block_byref_person_0中多了两个函数,通过其初始化可以知道这两个函数分别是__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
这两个函数,这两个函数就是在block从栈区复制到堆区时,实现person指针对person对象的引用,也就是图中第二个箭头。我们搜索一下这两个函数:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
这两个函数其实和_main_block_copy_0和_main_block_dispose_0一样,最终都是调用_Block_object_assign和_Block_object_dispose这两个函数。那么这里为什么都加上了40呢?我们分析一下_Block_byref_person_0的结构:
struct __Block_byref_person_0 {
void *__isa; //指针,8字节
__Block_byref_person_0 *__forwarding; //指针,8字节
int __flags; //int型,4字节
int __size; //int型,4字节
void (*__Block_byref_id_object_copy)(void*, void*);//指针型,8字节
void (*__Block_byref_id_object_dispose)(void*);//指针型,8字节
Person *person;
};
这样一来,_Block_byref_person_0的地址和person指针的地址就相差40字节,所以+40的目的就是找到person指针。
如果我们对__block修饰的对象类型用__weak类型来修饰会怎样呢?
PDBlock block;
Person *person0 = [[Person alloc] init];
__block __weak Person *person = person0;
block = ^{
NSLog(@"%@", person);
};
我们从c++的源码查找一下不同之处:
struct __Block_byref_person_0 {
void *__isa;
__Block_byref_person_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__weak person;
};
发现不同之处只体现在_Block_byref_person_0这个结构体上,这个结构体的person指针变成了使用__weak修饰了,也就是person指针对person对象产生弱引用,即上图中第二条线是虚线,弱引用。
要验证person指针对person对象是强引用还是若引用非常简单
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block;
{
Person *person0 = [[Person alloc] init];
__block __weak Person *person = person0;
block = ^{
NSLog(@"%@", person);
};
}
//在这里打断点
}
return 0;
}
我们在代码中标记的位置打断点,发现执行在断点时已经打印了--------dealloc
,说明person对象出了大括号就被释放了,所以_Block_byref_person_0结构体中的person指针对person对象是弱引用。
如果代码是这样:
int main(int argc, char * argv[]) {
@autoreleasepool {
PDBlock block;
{
Person *person = [[Person alloc] init];
block = ^{
NSLog(@"%@", person);
};
}
//在这里打断点
}
return 0;
}
那么代码执行到断点处是还没有打印--------dealloc
,说明此时person对象还没有被释放,也即person指针对person对象有强引用的作用。
6.Block中循环引用的问题
首先看一个循环引用的例子:
//Person.h
typedef void(^PDBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy)PDBlock mblock;
@property (nonatomic, assign)NSInteger age;
@end
//main.m
int main(int argc, char * argv[]) {
@autoreleasepool {
{
Person *person = [[Person alloc] init];
person.mblock = ^{
NSLog(@"%@", person);
};
}
//在这里打断点
}
return 0;
}
这段代码写完,编译器会提示会出现循环引用,那么我们一起来分析一下它们之间的相互引用问题。
首先分析这个block:
^{
NSLog(@"%@", person);
};
在这个block中,由于使用了对象类型的auto变量,所以block结构体中有一个成员变量person指针,这个person指针指向的是在外面利用alloc创建的person对象,并且有强引用,用图表示就是这样:
接下来我们再来分析一下person对象,这个person对象本质也是一个结构体,这个结构体中就有成员变量mblock,mblock又对上面的block是强引用,同时外面创建的person指针也会强引用alloc出来的person对象,所以总的引用关系图就是这样的:
当代码运行到代码中的断点处时,并没有打印--------dealloc
,这说明这个时候person对象还没有被释放,我们希望的是person对象出了大括号也就是作用域就被释放,不然就是内存泄漏了,那么这里为什么会内存泄漏呢?上图中,当出了大括号时,图中的红色箭头就没有了,红色箭头没有之后就变成了block强引用person对象,person对象强引用block,形成循环引用,就这样person对象的内存就一直不能被释放。
循环引用的解决办法
要解决循环引用这个问题,很明确就要从上面的绿箭头和黑箭头上想办法,如果把其中之一变成虚线,也即是弱引用,那循环引用就解决了。
那这个时候一个合适的方法就是把黑色的箭头变成虚线。
__weak,__unsafe_unretained
使用__weak或者__unsafe_unretained创建weakPerson对象,这样block结构体内的成员变量person指针就是一个弱指针,就能使黑色箭头变成虚线:
int main(int argc, char * argv[]) {
@autoreleasepool {
{
Person *person = [[Person alloc] init];
__weak typeof(person) weakperson = person;
person.mblock = ^{
NSLog(@"%@", weakperson);
};
}
//在这里打断点
}
return 0;
}
这样代码执行到断点处已经打印了--------dealloc
,说明循环引用已经解决了。
用__block解决(必须要调用block)
使用__block,,它们之间的相互引用是这样的:
那么有人就好奇了,这不是一个更大的循环引用吗?这确实是一个循环引用,但是如果block执行了,也即将auto类型的自动变量置为nil,那么这条蓝线就消失了,就打破了循环引用。