上文讲了一下__block的原理,但是关于__block还有一些其他东西,这篇文章就来慢慢讲述。
一. __block变量的内存管理
通过上文我们知道,block捕获对象类型的auto变量就会多出两个函数用于做内存管理操作(__main_block_copy_0和__main_block_dispose_0),如下:
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_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
block也会使用这两个函数管理__block修饰的变量的内存,这也从侧面证明“编译器会将__block变量包装成一个对象”,这句话是对的,因为只有对象才需要内存管理。
下面我们就研究使用__block修饰的变量的内存管理:
- 当block在栈上时,并不会对__block变量产生强引用
- 当block被copy到堆时(自己拷贝的或者ARC下系统自动拷贝的)
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会对__block变量形成强引用(retain)
如下图:
解释:刚开始的时候,__block变量和block0、block1肯定都在栈区,假如他们同时使用__block变量。
当把block0复制到堆区,也会把__block变量复制到堆区,并且block0强引用__block变量。
再把block1复制到堆区,就不会再次复制__block变量了(因为已经拷贝过了),这时候block0和block1都会强引用着__block变量。
- 当block从堆中移除时
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的__block变量(release)
如下图:
解释:当没有block引用着__block变量,__block变量才会被释放。
为什么要这么管理呢?
因为__block变量是个对象,block内部使用了它,所以需要block来管理它的内存。
二. 对象类型的auto变量和__block变量的区别
(static修饰的变量和全局变量在全局区)
先把下面代码转成C++代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int no = 20;
__block int age = 10;
NSObject *object = [[NSObject alloc] init];
__weak NSObject *weakObject = object;
MJBlock block = ^{
age = 20;
NSLog(@"%d", no);
NSLog(@"%d", age);
NSLog(@"%p", weakObject);
};
block();
}
return 0;
}
转成的C++代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int no;
NSObject *__weak weakObject;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _no, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, int flags=0) : no(_no), weakObject(_weakObject), age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
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_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
可以发现,结构体多了三个成员,分别对应捕获的三个auto变量,如下👇:
int no;
__Block_byref_age_0 *age; // by ref
NSObject *__weak weakObject;
1. 对象类型的auto变量、__block变量
相同点:
- 当block在栈上时,对它们都不会产生强引用
- 当block拷贝到堆上时,都会通过copy函数来处理它们
对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
__block变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
- 当block从堆上移除时,都会通过dispose函数来释放它们
对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
__block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
不同点:
- 对于对象类型的auto变量,_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
- 对于__block变量:_Block_object_assign函数只会产生强引用(retain)
2. 普通的的auto变量
例如:int a = 10,就是值引用,然后放到__main_block_impl_0结构体里面。
三. __forwarding指针的作用
我们知道__Block_byref_age_0结构体中的__forwarding指针存的是自己的地址,当我们想要访问age,需要先通过age结构体中的__forwarding获取自己,然后再获取age:
(age->__forwarding->age) = 20;
为什么设计这么奇怪呢?如下图:
解释:如果栈上的block复制到堆上了,那么栈上堆上肯定都有一块内存。如果我们想把20赋值到堆上的block,如果不用__forwarding指针,访问栈上的block,就会把20赋值到栈上的block。如果使用__forwarding指针,不管访问的block在哪,最后赋值到的一定是堆上的block。
四. __block修饰的对象类型
以前我们是使用__block修饰基本数据类型,如果使用__block修饰对象类型会怎么样呢?
如下代码,内部会发生什么呢?
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
//不能反过来(如:__weak __block),因为__weak只能修饰OC对象
__block __weak MJPerson *weakPerson = person;
MJBlock block = ^{
NSLog(@"%p", weakPerson);
};
block();
}
return 0;
}
转成C++代码,如下:
struct __Block_byref_weakPerson_0 {
void *__isa; // 8
__Block_byref_weakPerson_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
MJPerson *__weak weakPerson; //外面是弱指针,这里就是弱指针 (以前这里直接是int age;的)
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_weakPerson_0 *weakPerson; // 默认强指针
__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里面默认有一个强指针指向__Block_byref_weakPerson_0结构体。由于外面是用__weak修饰的,所以__Block_byref_weakPerson_0结构体里面有一个弱指针指向person对象,如下图:
上面的代码还可以看出__Block_byref_weakPerson_0结构体里面多了两个方法copy和dispose方法。(这两个方法在以前我们使用__block修饰基本数据类型的时候是没有的)
在__Block_byref_weakPerson_0结构体创建里面(就是__block __weak MJPerson *weakPerson = person;的底层实现),传入了copy和dispose两个函数地址,如下:
//copy block内存管理相关
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init"));
//__block __weak MJPerson *weakPerson = person;的底层实现
//就是创建__Block_byref_weakPerson_0结构体
__Block_byref_weakPerson_0 weakPerson = {(void*)0,(__Block_byref_weakPerson_0 *)&weakPerson, 33554432, sizeof(__Block_byref_weakPerson_0),
__Block_byref_id_object_copy_131, //传入copy方法的地址
__Block_byref_id_object_dispose_131, //传入dispose方法的地址
person};
MJBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_weakPerson_0 *)&weakPerson, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
struct __Block_byref_weakPerson_0 {
void *__isa; // 8
__Block_byref_weakPerson_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
MJPerson *__weak weakPerson; //外面是弱指针,这里就是弱指针 (以前这里直接是int age;的)
};
//copy 对象内存管理相关
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
//dispose
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
观察上面的copy,发现它里面也有调用_Block_object_assign函数,(char*)dst就是结构体自己的地址值,但是它却+40 (8+8+4+4+8+8),从上上面我们分析可知:(char*)dst+40就是“MJPerson *__weak weakPerson;”的地址值。然后_Block_object_assign函数再根据传入的对象是强还是弱,再决定retain还是弱引用。所以上图的第二根指针的强弱,取决于外面修饰对象的的是强指针还是弱指针?
当block销毁的时候,block内部会调用__main_block_dispose_0函数,然后上图的第一根线就没了。第一根线就没了,__Block_byref_weakPerson_0也会销毁,它也会调用自己里面的__Block_byref_id_object_dispose_131函数,__Block_byref_id_object_dispose_131函数再把person引用计数器减一(或者销毁)。
总结:
对于block:
- 如果block是在栈上,将不会对__Block_byref_weakPerson_0产生强引用
- 如果栈上的block被拷贝到堆上
_Block_object_assign函数会对__Block_byref_weakPerson_0产生强引用 - 如果堆上的block被移除
_Block_object_dispose函数会对__Block_byref_weakPerson_0产生弱引用或者移除
对于__block修饰的对象类型:
- 当__block变量在栈上时,不会对指向的对象产生强引用
- 当__block变量被copy到堆时(自己拷贝的或者ARC下系统自动拷贝的)
会调用__block变量内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain,一直是弱的。这个特例只会在MRC并且是__block修饰对象类型才有) - 如果__block变量从堆上移除
会调用__block变量内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)
下面验证上面的“注意”:
将项目切换成MRC环境,使用__block修饰对象类型,如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block MJPerson *person = [[MJPerson alloc] init];
MJBlock block = [^{
NSLog(@"%p", person);
} copy];
[person release];
block(); //在此处打断点
[block release];
}
return 0;
}
在上面代码断点处,可以发现打印:“[MJPerson dealloc]”,此时block还没释放,但是person却不在了,验证了我们上面说的“注意”。根据这一点,在MRC环境下,我们可以使用__block来解决循环引用,这个后面会说到。
解释如上代码,当block被拷贝到堆上的时候,__block修饰的person也会被拷贝到堆上,这时候会调用__block修饰的变量内部的copy函数,MRC环境下,copy函数内部只会对person对象产生弱引用。如下图:
如果将上面代码的__block去掉,那么在断点处就不会打印:“[MJPerson dealloc]”,之后block释放之后person才会释放。这时候就是block直接捕获person对象了,如下图:
五. block循环引用
1. 循环引用产生的原因
如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"age is %d", person.age);
};
}
NSLog(@"111111111111");
return 0;
}
首先“ MJPerson *person = [[MJPerson alloc] init];”执行完,会有个person强指针指向MJPerson。MJPerson里面有个_block,当执行完 person.block = ^{NSLog(@"age is %d", person.age);},MJPerson和block之间的循环引用就会产生,如下图:
(block是封装了函数调用以及函数调用环境的OC对象)
同理,self和block之间的循环引用如下图:
因为self是局部变量,所以block也会捕获self,也会造成循环引用。block底层C++代码如下:
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *const __strong self; //block内部有个self强指针,指向self对象
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
2. ARC如何解决循环引用
① 用__weak解决
首先,我们想一下,如何解决循环引用,肯定是把其中一根线变成虚线就可以了,但是把哪根线变成虚线呢?我们想了下,person中的block属性不能变成虚的,因为person要拥有block,那么只能把block里面的person变成虚线了,怎么把block里面的person变成虚线呢?其实就是在上面的代码,把__strong变成__weak就好了,前面我们已经学过了,在外面用__weak修饰,里面就会变成__weak了,如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
//__weak MJPerson *weakPerson = person; //这样写死了,不推荐
__weak typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"age is %d", weakPerson.age);
};
}
NSLog(@"111111111111");
return 0;
}
这时候block底层C++代码如下:
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *const __weak weakPerson; //weakPerson弱指针
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
使用__weak后图示如下:
解释:当{}里面的代码执行完,MJPerson *person局部变量就会消失,person指向MJPerson的线就会消失,这时候没有实线指向MJPerson,MJPerson就会销毁,MJPerson销毁后,MJPerson指向block的线就会消失,这时候没有实线指向block,block也会销毁,循环引用解除。
② 用__unsafe_unretained解决
上面的代码,把__weak替换成__unsafe_unretained也能解决循环引用,代码就省略了。
它们有什么区别呢?直接看字面意思,__weak:不会产生强引用,__unsafe_unretained:不会产生强引用,不安全。
__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil
__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,这时候如果再去访问指针指向的地址就会报野指针错误
如下图,如果使用__weak,当weakPerson指向的对象销毁时,会把weakPerson置为nil,使用__unsafe_unretained就不会。
③ 用__block解决(必须要调用block)
int main(int argc, const char * argv[]) {
@autoreleasepool {
//使用__block解决循环引用
__block MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"age is %d", person.age);
person = nil;
};
person.block();
}
NSLog(@"111111111111");
return 0;
}
如上代码,即可解决循环引用。
首先我们分析这里的循环引用是如何产生的。
前面我们说过使用__block修饰对象,block内部会捕获__block变量,__block变量内部又有person对象,而且默认是强引用,对象又拥有block,所以block、__block变量、person对象之间会有循环引用,示意图如下:
所以,如果想要解决__block的循环引用,我们可以调用block,并且在block里面把person对象置为nil,这样__block变量就不会持有person对象了,循环引用被打破,代码如上,示意图如下:(以前我们也验证过,__block变量里的person指针就是我们在外面拿到的person指针)
总结:不用想也知道ARC环境下使用__weak解决循环引用最好。
3. MRC如何解决循环引用
我们知道MRC不支持使用__weak的,否则报错“Cannot create __weak reference in file using manual reference counting”
所以,相对于ARC,MRC就只有两种方式解决循环引用了。
① 使用__unsafe_unretained解决
int main(int argc, const char * argv[]) {
@autoreleasepool {
// MRC不支持__weak的否则报错:👇
// Cannot create __weak reference in file using manual reference counting
__unsafe_unretained MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.block = [^{
NSLog(@"age is %d", person.age);
} copy];
[person release];
}
NSLog(@"111111111111");
return 0;
}
如上代码,使用__unsafe_unretained修饰person,当block引用person的时候,person的引用计数器不会增加,还是刚开始创建的时候的1,所以当“ [person release];”person就挂了。
如果不使用__unsafe_unretained修饰,person创建的时候引用计数器为1,因为person默认强指针,所以block引用person的时候又加1,所以当“ [person release];”person引用计数器还是1,不会挂。
② 使用__block解决
还记得上面验证的“注意”吗?MRC环境,使用__block修饰对象类型,对象不会被retain,所以也可以解决循环引用。
代码省略,可自行将上面的__unsafe_unretained替换成__block进行验证。
补充1:block属性的建议写法
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
- 使用copy会将栈上的block拷贝到堆上,如果不使用copy,block就不会拷贝到堆上,因为MRC编译器不会自动copy,所以只能用copy。
- 以前我们说过,ARC环境,并且把block赋值给强指针,编译器会自动把block拷贝到堆上,所以ARC使用copy和strong都可以,没区别。
- 为了统一好记,我们统一对block使用copy。
为什么一定要copy到堆上呢?如果在栈上我们就无法控制block的生命周期,在堆上,什么时候让它生让它死都可以。
补充2:__strong typeof(weakSelf) myself = weakSelf;
为什么要这么写:
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) myself = weakSelf;
//报错:
//Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first
NSLog(@"age is %d", myself->_age);
};
- 原因一,如果在block内部通过weakSelf->_age会报错“弱指针不允许访问,因为有可能为null,让你使用强指针”,所以我们就用__strong强指针来访问self。
- 原因二,访问self的时候我们使用一个临时的强指针来访问self,这样在整个block执行期间,可以保证self对象不会被销毁,同时,block调用完后,临时的强指针被销毁,一切又回归原来的样子。这样既能保证整个block执行期间,self对象不会被销毁,又能保证不会产生循环引用。
面试题:
block的原理是怎样的?本质是什么?
block是封装了函数调用以及调用环境的OC对象,比如函数的调用地址、捕获的变量都封装到了里面。__block的作用是什么?有什么使用注意点?
编译器会将__block变量包装成一个对象,就是__Block_byref_person_0这种结构体,可以解决block内部无法修改auto变量的问题(自己思考为什么不能修改)。
使用注意:__block变量内部自己也会进行内存管理,而且MRC环境下,__block修饰对象,对象不会被retain的。为什么使用__block修饰auto变量,在block内部就能修改此变量了?
block内部有个指针指向__Block_byref_person_0结构体,通过访问结构体,再通过结构体访问变量进行修改的。block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,就不会在堆上。使用注意:循环引用问题。block在修改NSMutableArray,需不需要添加__block?
NSMutableArray *array = [NSMutableArray array];
person.block = ^{
[array addObject:@"123"];
};
不需要。
如上代码,这个我们以前说过,给array数组添加成员只是操作array,除非要把array指针指向其他地方才要使用__block(比如:array = nil)。
一般如果没必要不要用__block修饰,因为还要包装一层。
Demo地址:block循环引用