书接上文 : block的原理篇 : https://blog.csdn.net/u014600626/article/details/78697535
__block实现原理
我们继续研究一下__block实现原理。
__block修饰非对象的变量
先来看看普通变量的情况。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
__block int i = 0;
void (^myBlock)(void) = ^{
i ++;
NSLog(@"%d",i);
};
myBlock();
return 0;
}
把上述代码用clang转换成源码。
// 加了__block后 i被封装成一个对象(结构体)
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding; //
int __flags;
int __size;
int i; // 真实的i的值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // block持有这个结构体
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref
(i->__forwarding->i) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
}
从源码我们能发现,带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,这个结构体有5个成员变量。第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。
至此,__block的实现原理也已经明了。__block
将变量包装成对象,然后在把捕获到的变量封装在block的结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。
由此在引出来一个问题 : __block
修饰的变量在编译时会被封装为结构体,那么当在外部使用同一个
变量的时候,使用的是int age呢?还是__Block_byref_age_0
结构体内的age->__forwarding->age变量呢?
为了验证上述问题
同样使用自定义结构体的方式来查看其内部结构
typedef void (^Block)(void);
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age; // by ref
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
Block block = ^{
age = 20;
NSLog(@"age is %d",age);
};
block();
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
NSLog(@"%p",&age);
}
return 0;
}
打印断点查看结构体内部结构
_Block_byref_age_0结构体
通过查看blockImpl结构体其中的内容,找到age
结构体,其中重点观察两个元素:
__forwarding
其中存储的地址是age结构体变量自己的地址age
中存储这修改后的变量20。
上面也提到过,在block
中使用或修改age
的时候都是通过结构体__Block_byref_age_0
找到__forwarding
在找到变量age
的。
另外apple为了隐藏__Block_byref_age_0
结构体的实现,打印age变量的地址发现其实是__Block_byref_age_0
结构体内age
变量的地址。
通过上图的计算可以发现打印age
的地址同__Block_byref_age_0
结构体内age
值的地址相同。也就是说外面使用的age,就是结构体内的age。
__block修饰对象类型
那么如果变量本身就是对象类型呢?通过以下代码生成c++源码查看
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
NSLog(@"%@",person);
Block block = ^{
person = [[Person alloc] init];
NSLog(@"%@",person);
};
block();
}
return 0;
}
通过源码查看,将对象包装在一个新的结构体中。结构体内部会有一个person
对象,不一样的地方是结构体内部添加了内存管理的两个函数__Block_byref_id_object_copy
和__Block_byref_id_object_dispose . 下面就说说__block的内存管理
__block内存管理
上文提到当block中捕获对象类型的变量时,block中的__main_block_desc_0
结构体内部会自动添加copy
和dispose
函数对捕获的变量进行内存管理。
那么同样的当block内部捕获__block
修饰的对象类型的变量时,__Block_byref_person_0
结构体内部也会自动添加__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
对被__block
包装成结构体的对象进行内存管理。
当block
内存在栈上时,并不会对__block
变量产生内存管理。当blcok
被copy
到堆上时
会调用block
内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会对__block
变量形成强引用(相当于retain)
首先通过一张图看一下block复制到堆上时内存变化
__block copy内存管理
当block
被copy
到堆上时,block
内部引用的__block
变量也会被复制到堆上,并且持有变量,如果block
复制到堆上的同时,__block
变量已经存在堆上了,则不会复制。
当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0
函数,__main_block_dispose_0
函数内部会调用_Block_object_dispose
函数,会自动释放引用的__block变量。
__block 释放内存管理
block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。
__block
修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。
一段代码更深入的观察一下。
首先需要说明的一点是对象在OC中,默认声明自带__strong所有权修饰符的 .
在转换出来的源码中,我们也可以看到,Block捕获了__block,并且强引用了,因为在__Block_byref_person_1结构体中,有一个变量是 Person *__strong person; ,这个默认也是带__strong所有权修饰符的。
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int number = 20;
__block int age = 10;
NSObject *object = [[NSObject alloc] init];
__weak NSObject *weakObj = object;
Person *p = [[Person alloc] init];
__block Person *person = p;
__block __weak Person *weakPerson = p;
Block block = ^ {
NSLog(@"%d",number); // 局部变量
NSLog(@"%d",age); // __block修饰的局部变量
NSLog(@"%p",object); // 对象类型的局部变量
NSLog(@"%p",weakObj); // __weak修饰的对象类型的局部变量
NSLog(@"%p",person); // __block修饰的对象类型的局部变量
NSLog(@"%p",weakPerson); // __block,__weak修饰的对象类型的局部变量
};
block();
}
return 0;
}
将上述代码转化为c++代码查看不同变量之间的区别
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int number;
NSObject *__strong object;
NSObject *__weak weakObj;
__Block_byref_age_0 *age; // by ref
__Block_byref_person_1 *person; // by ref
__Block_byref_weakPerson_2 *weakPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
上述__main_block_impl_0
结构体中看出,没有使用__block
修饰的变量(object 和 weadObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block
修饰的变量,__main_block_impl_0
结构体内一律使用强指针引用生成的结构体。
接着我们来看__block
修饰的变量生成的结构体有什么不同
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_person_1 {
void *__isa;
__Block_byref_person_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__strong person;
};
struct __Block_byref_weakPerson_2 {
void *__isa;
__Block_byref_weakPerson_2 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__weak weakPerson;
};
如上面分析的那样,__block
修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
两个函数,用来对对象类型的变量进行内存管理的操作。而结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson
是弱指针,所以__Block_byref_weakPerson_2
对weakPerson
就是弱引用,person
是强指针,所以__Block_byref_person_1
对person就是强引用。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->age, (void*)src->age, 8);
_Block_object_assign((void*)&dst->object, (void*)src->object, 3);
_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3);
_Block_object_assign((void*)&dst->person, (void*)src->person, 8);
_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8);
}
__main_block_copy_0
函数中会根据变量是强弱指针及有没有被__block
修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block
修饰的变量最后的参数传入的是8,没有被__block
修饰的变量最后的参数传入的是3。
当block从堆中移除时通过dispose函数来释放他们。
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->age, 8);
_Block_object_dispose((void*)src->object, 3);
_Block_object_dispose((void*)src->weakObj, 3);
_Block_object_dispose((void*)src->person, 8);
_Block_object_dispose((void*)src->weakPerson, 8);
}
__forwarding指针
上面提到过__forwarding
指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding
指针,在通过__forwarding
指针找到相应的变量。这样设计的目的是为了方便内存管理。通过上面对__block
变量的内存管理分析我们知道,block
被复制到堆上时,会将block
中引用的变量也复制到堆中。
我们重回到源码中。当在block中修改__block
修饰的变量时。
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_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_main_b05610_mi_0,(age->__forwarding->age));
}
通过源码可以知道,当修改__block
修饰的变量时,是根据变量生成的结构体这里是__Block_byref_age_0
找到其中__forwarding
指针,__forwarding
指针指向的是结构体自己因此可以找到age变量进行修改。
当block在栈中时,__Block_byref_age_0
结构体内的__forwarding
指针指向结构体自己。
而当block被复制到堆中时,栈中的__Block_byref_age_0
结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0
结构体中的__forwarding
指针指向的就是堆中的__Block_byref_age_0
结构体,堆中__Block_byref_age_0
结构体内的__forwarding
指针依然指向自己。
此时当对age进行修改时
// 栈中的age
__Block_byref_age_0 *age = __cself->age; // bound by ref
// age->__forwarding获取堆中的age结构体
// age->__forwarding->age 修改堆中age结构体的age变量
(age->__forwarding->age) = 20;
通过__forwarding
指针巧妙的将修改的变量赋值在堆中的__Block_byref_age_0
中。
我们通过一张图展示__forwarding
指针的作用
__forwarding指针
因此block内部拿到的变量实际就是在堆上的。当block进行copy被复制到堆上时,_Block_object_assign
函数内做的这一系列操作。
被__block修饰的对象类型的内存管理
使用以下代码,生成c++代码查看内部实现
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
Block block = ^ {
NSLog(@"%p", person);
};
block();
}
return 0;
}
来到源码查看__Block_byref_person_0
结构体及其声明
__Block_byref_person_0结构体
typedef void (*Block)(void);
struct __Block_byref_person_0 {
void *__isa; // 8 内存空间
__Block_byref_person_0 *__forwarding; // 8
int __flags; // 4
int __size; // 4
void (*__Block_byref_id_object_copy)(void*, void*); // 8
void (*__Block_byref_id_object_dispose)(void*); // 8
Person *__strong person; // 8
};
// 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48
// __Block_byref_person_0结构体声明
__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 (*__Block_byref_id_object_copy)(void*, void*);
和void (*__Block_byref_id_object_dispose)(void*);
。这两个函数为__block
修饰的对象提供了内存管理的操作。
可以看出为void (*__Block_byref_id_object_copy)(void*, void*);
和void (*__Block_byref_id_object_dispose)(void*);
赋值的分别为__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
。找到这两个函数
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);
}
上述源码中可以发现__Block_byref_id_object_copy_131
函数中同样调用了_Block_object_assign
函数,而_Block_object_assign
函数内部拿到dst
指针即block
对象自己的地址值加上40个字节。并且_Block_object_assign
最后传入的参数是131,同block直接对对象进行内存管理传入的参数3,8都不同。可以猜想_Block_object_assign
内部根据传入的参数不同进行不同的操作的。
通过对上面__Block_byref_person_0
结构体占用空间计算发现__Block_byref_person_0
结构体占用的空间为48个字节。而加40恰好指向的就为person
指针。
也就是说copy函数会将person地址传入_Block_object_assign
函数,_Block_object_assign
中对Person对象进行强引用或者弱引用。
强引用示意图
如果使用__weak修饰变量查看一下其中的源码
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
__block __weak Person *weakPerson = person;
Block block = ^ {
NSLog(@"%p", weakPerson);
};
block();
}
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_weakPerson_0 *weakPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0
中没有任何变化,__main_block_impl_0
对weakPerson
依然是强引用,但是__Block_byref_weakPerson_0
中对weakPerson
变为了__weak
指针。
struct __Block_byref_weakPerson_0 {
void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__weak weakPerson;
};
也就是说无论如何block
内部中对__block
修饰变量生成的结构体都是强引用,结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。
弱引用示意图
mrc环境下,尽管调用了copy操作,__block
结构体不会对person
产生强引用,依然是弱引用。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
Block block = [^ {
NSLog(@"%p", person);
} copy];
[person release];
block();
[block release];
}
return 0;
}
上述代码person会先释放
block的copy[50480:8737001] -[Person dealloc]
block的copy[50480:8737001] 0x100669a50
当block从堆中移除的时候。会调用dispose
函数,block块中去除对__Block_byref_person_0 *person;
的引用,__Block_byref_person_0
结构体中也会调用dispose
操作去除对Person *person;
的引用。以保证结构体和结构体内部的对象可以正常释放。
循环引用
循环引用导致内存泄漏。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"%d",person.age);
};
}
NSLog(@"大括号结束啦");
return 0;
}
运行代码打印内容
block的copy[55423:9158212] 大括号结束啦
可以发现大括号结束之后,person
依然没有被释放,产生了循环引用。
通过一张图看一下他们之间的内存结构
产生循环引用示意图
上图中可以发现,Person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏。
解决循环引用问题 - ARC
首先为了能随时执行block,我们肯定希望person
对block对强引用,而block内部对person
的引用为弱引用最好。
使用__weak
和 __unsafe_unretained
修饰符可以解决循环引用的问题
我们上面也提到过__weak
会使block
内部将指针变为弱指针。block
对person
对象为弱指针的话,也就不会出现相互引用而导致不会被释放了。
使用`__weak`和`__unsafe_unretained`修饰
__weak
和 __unsafe_unretained
的区别。
__weak
不会产生强引用,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak
来解决问题。
__unsafe_unretained
不会产生前引用,不安全,指向的对象销毁时,指针存储的地址值不变。
使用__block
也可以解决循环引用的问题。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"%d",person.age);
person = nil;
};
person.block();
}
NSLog(@"大括号结束啦");
return 0;
}
上述代码之间的相互引用可以使用下图表示
使用__block也可以解决循环引用
上面我们提到过,在block内部使用变量使用的其实是__block
修饰的变量生成的结构体__Block_byref_person_0
内部的person
对象,那么当person
对象置为nil也就断开了结构体对person的强引用,那么三角的循环引用就自动断开。该释放的时候就会释放了。但是有弊端,必须执行block,并且在block内部将person
对象置为nil。也就是说在block执行之前代码是因为循环引用导致内存泄漏的。
__strong 和 __weak
__weak typeof(self) weakSelf = self;
person.block = ^{
__strong typeof(weakSelf) myself = weakSelf;
NSLog(@"age is %d", myself->_age);
};
在block
内部重新使用__strong
修饰self
变量是为了在block
内部有一个强指针指向weakSelf
避免在block
调用的时候weakSelf
已经被销毁。主要实在多线程中使用 , 实际开发环境中 , 线程比较简单 , __strong ,一般可省略. 还是用一个例子来说明吧.
如果block里面不加__strong __typeof(weakSelf)strongSelf = weakSelf会如何呢?
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
__weak typeof(student) weakStu = student;
student.study = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"my name is = %@",weakStu.name);
});
};
student.study();
}
输出:
my name is = (null)
为什么输出是这样的呢?
重点就在dispatch_after这个函数里面。在study()的block结束之后,student被自动释放了。又由于dispatch_after里面捕获的__weak的student,根据__weak的实现原理,在原对象释放之后,__weak对象就会变成null,防止野指针。所以就输出了null了。
那么我们怎么才能在weakSelf之后,block里面还能继续使用weakSelf之后的对象呢?
究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加__strong。
在block里面使用的__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
__weak typeof(student) weakSelf = student;
student.study = ^{
__strong typeof(student) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"my name is = %@",strongSelf.name);
});
};
student.study();
}
输出:
my name is = Hello World
至此,我们就明白了weakSelf、strongSelf的用途了。
weakSelf 是为了block不持有self,避免Retain Circle循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。
strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内有多线程的或者block的嵌套访问 self,则需要使用 strongSelf。
为什么Masonry中使用self,不会引起循环应用??
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.mas_left).mas_offset(0);
make.right.equalTo(self.view.mas_right).mas_offset(0);
make.bottom.equalTo(self.save_btn.mas_top).mas_offset(0);
make.top.equalTo(self.view.mas_top).mas_offset(0);
}];
看到这里,读者应该就应该能回答这个问题了。
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
block内部会引用变量 self,闭包虽然引用了self,但是self并没有引用这个闭包,self.tableview也没有引用 , 没有任何变量引用了block , block只是一个局部参数 , 函数执行完毕后 , block的内存就被系统回收了. 而且还有更骚的地方 , 这个block 是一个__NSStackBlock__ . 虽然是在arc环境下 , 但是由于没有赋值 , 没有调copy 所以这个是__NSStackBlock__,自然也没有引起循环依赖。