上篇文章只是简单讲了MRC环境下block的copy操作。
一. ARC环境下,block的copy操作
接下来我们讲的都是在ARC环境下。
观察如下代码:
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
//block变量强引用着右边的block
MJBlock block = ^{
NSLog(@"---------%d", age);
};
block();
NSLog(@"%@", [block class]);
}
return 0;
}
打印:
---------10
__NSMallocBlock__
上文我们说过如果block访问了atuo变量就是__NSStackBlock__,存放在栈区,栈区的内存系统自动管理,那么在{}结束后block就被销毁了,这时候再访问block就是很危险的事,上面block也没有进行copy操作,但是现在为什么可以打印呢?
这是因为我们现在在ARC环境下,并且将block赋值给强指针指着了,编译器帮我们做了copy操作,将栈上的block复制到堆上,所以上面的打印才是__NSMallocBlock__类型。
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
- block作为函数返回值时
- 将block赋值给__strong指针时
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- block作为GCD API的方法参数时
前两种情况比较好理解,就不解释了,后面两种情况看如下代码:
NSArray *arr = @[];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//block作为Cocoa API中方法名含有usingBlock的方法参数时
}];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//block作为GCD API的方法参数时
});
二. 对象类型的auto变量
先看几个例子:
如下代码:
- MRC 不copy
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJBlock block;
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"---------%d", person.age);
};
//如果是MRC person离开{}之前要进行release
[person release];
}
NSLog(@"------"); //此处打断点,block还在,person被销毁了
}
return 0;
}
打印: MJPerson - dealloc ,person被释放。可以发现block还在,但是离开{}之后person就被释放掉了
- MRC copy
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJBlock block;
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
block = [^{
NSLog(@"---------%d", person.age);
} copy];
//如果是MRC person离开{}之前要进行release
[person release];
}
NSLog(@"------");//打断点
}
return 0;
}
没打印,person没被释放。所以我们猜想在MRC环境下,copy操作之后,block内部对person做了[person retain]操作,所以person没被销毁。
- ARC环境
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJBlock block;
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"---------%d", person.age);
};
}
NSLog(@"------");//打断点
}
return 0;
}
ARC环境下,在NSLog处打断点,发现执行到NSLog,person对象没有调用dealloc方法,person没被释放。
这是因为:上面的block捕获了auto变量(MJPerson *person,ARC环境下默认是强引用的,如下所示:)所以是NSStackBlock,在栈空间。又因为是ARC环境并且block有强指针指着,所以编译器把block自动copy了一下,变成了NSMallocBlock,在堆空间,堆空间的block就不会随便被销毁了,所以block会一直存在,又因为block内部又有捕获的person指针指向person对象,如下,所以走到断点的时候,person对象不会被释放。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MJPerson *__strong person; //ARC环境下,默认强引用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
小总结:不管是MRC还是ARC,栈空间的block是不会保住捕获的变量的命,堆空间的block可以保住捕获的变量的命。
- ARC __weak
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJBlock block;
{
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
__weak MJPerson *weakPerson = person;
block = ^{
NSLog(@"---------%d", weakPerson.age);
};
}
NSLog(@"------");//打断点
}
return 0;
}
打印: MJPerson - dealloc ,person被释放。
ARC环境下,使用__weak修饰,发现person又被释放了,相信看完上面的各种例子也有点懵了,下面进行大总结:
大总结:
无论MRC、ARC:
当block内部访问了对象类型的auto变量时
如果block是在栈上,将不会对auto变量产生强引用
如果栈上的block被拷贝到堆上
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用如果堆上的block被移除
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的auto变量(或者release)
下面将代码转成C++代码,验证刚才的大总结:
typedef void (*MJBlock)(void);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MJPerson *__strong person;//捕获的变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
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) {
//函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
//函数会自动释放引用的auto变量(release)
_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //block大小
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};
//初始化传入了__main_block_copy_0函数和__main_block_dispose_0函数的地址
我们主要看__main_block_desc_0,可以发现当捕获的是个对象时,这个结构体就多了三、四两个成员,初始化的时候,第三个成员传入__main_block_copy_0函数的地址,第四个成员传入__main_block_dispose_0函数的地址。为什么当捕获的是个对象就会多着两个函数呢?这也比较容易理解,既然捕获了对象,就要有内存管理相关了,所以这两个函数就需要了。这两个函数的作用可看上面注释,验证了我们上面的大总结:
下面用几个小题目测试p什么时候释放。
- 测试
- 案例一
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
MJPerson *p = [[MJPerson alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"-------%@", p);
});
NSLog(@"touchesBegan:withEvent:");
}
打印如下:
2019-11-29 16:15:44.964850+0800 Interview03-测试[65454:6090594] touchesBegan:withEvent:
2019-11-29 16:15:47.965174+0800 Interview03-测试[65454:6090594] -------
2019-11-29 16:15:47.965517+0800 Interview03-测试[65454:6090594] MJPerson - dealloc
点击空白之后,发现p不是立马被释放,而是3秒之后被释放了。为什么呢?
因为ARC环境下dispatch_after会默认对block进行Copy操作,从栈区Copy到堆区的时候,block内部会调用_Block_object_assign,又因为p默认是强引用,所以_Block_object_assign函数会对p进行retain操作,所以3秒后block销毁的时候p才会销毁。
- 案例二
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
MJPerson *p = [[MJPerson alloc] init];
__weak MJPerson *weakP = p;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1-------%@",p);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2-------%@", weakP);
});
});
NSLog(@"touchesBegan:withEvent:");
}
person对象在1s后释放
2019-12-02 09:12:06 touchesBegan:withEvent:
2019-12-02 09:12:07 1-------
2019-12-02 09:12:07 MJPerson - dealloc
2019-12-02 09:12:09 2-------(null)
因为外面的block捕获了p,并且是强引用,所以p会在外面的block执行完毕释放,所以是1s后
- 案例三
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
MJPerson *p = [[MJPerson alloc] init];
__weak MJPerson *weakP = p;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1-------%@", weakP);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2-------%@", p);
});
});
NSLog(@"touchesBegan:withEvent:");
}
因为里面的block捕获了p,并且是强引用,所以p会在里面的block执行完毕再释放,所以是3s后。
person对象在3s后释放
2019-12-02 09:13:29 touchesBegan:withEvent:
2019-12-02 09:13:30 1-------
2019-12-02 09:13:32 2-------
2019-12-02 09:13:32 MJPerson - dealloc
三. __block修饰变量
先看一个小案例:
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
MJBlock block2 = ^{
age = 30;
NSLog(@"age is %d", age);
};
block2();
}
return 0;
}
如上代码,运行会报错,Variable is not assignable (missing __block type specifier),意思是“变量不可赋值,缺少__block修饰”。
为什么不能改?
从上面我们分析C++代码可知,block里面的代码是在__main_block_func_0函数里面执行的,而age是定义在main函数里面的,两个函数的栈空间都不一样,肯定不能改。如果要改也只能改block结构体里面的age,但是main函数里面的age还是改不了啊。
那如何才能改?
① 使用static修饰:
上面的代码加static修饰“static int age = 20;”,发现可以修改,那为什么使用static修饰就可以改呢?还是查看C++代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *age = __cself->age; // bound by copy
(*age) = 30;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_42e634_mi_0, (*age));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int age = 20;
MJBlock block2 = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
}
return 0;
}
可以发现,结构体中使用了int *age来保存age的地址,在调用block的时候,内部执行__main_block_func_0函数,__main_block_func_0函数再访问age指针,再通过age指针将age值修改“int *age = __cself->age; (*age) = 30;”。
总结:指针传递,block内部可以修改外部成员变量的值。
② 使用全局变量
这个就更不用解释了,block结构体不会捕获全局变量,拿到全局变量直接改就是了。
有时候我们只是想临时改一下,并不想让变量一直在内存中,(如果使用static修饰变量会一直在内存中,全局变量也会一直在内存中),可以使用__block修饰。
③ 使用__block修饰
__block可以用于解决block内部无法修改auto变量值的问题
__block不能修饰全局变量、静态变量(static)(因为__block的作用就是上句)
编译器会将__block变量包装成一个对象
使用__block修饰“__block int age = 20;”就能在block内部修改外部变量的值,而且不会修改变量的性质(还是auto变量)。
那么为什么__block修饰修饰的可以修改呢?还是看C++代码
struct __Block_byref_age_0 {
void *__isa; //isa指针(指向类对象)
__Block_byref_age_0 *__forwarding; //自己类型的指针,后面可知道是指向自己
int __flags;
int __size; //自己的大小
int age; //age的值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; //指向__Block_byref_age_0结构体的指针
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
//先通过age指针拿到__forwarding指针(里面存的就是自己),再通过__forwarding指针拿到自己里面的值,然后修改值为30
(age->__forwarding->age) = 30;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_ad0be7_mi_0, (age->__forwarding->age));
}
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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//__block int age = 10;
__Block_byref_age_0 age = {
(void*)0,
(__Block_byref_age_0 *)&age, //自己的地址传给__forwarding(它内部有个指针指向自己)
0,
sizeof(__Block_byref_age_0),// 当前结构体多大
10
};
//声明block会把__Block_byref_age_0地址传过去
MJBlock block2 = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_age_0 *)&age,
570425344));
//调用block
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
}
return 0;
}
首先看__main_block_impl_0函数,定义block结构体的时候第三个参数是(__Block_byref_age_0 *)&age,这是指向__Block_byref_age_0结构体的指针,由于__Block_byref_age_0有个isa,我们可以认为它是个对象,里面保存了age的值,原来代码“__block int age = 10;”代码转成C++代码,就是如下__Block_byref_age_0结构体:
//__block int age = 10;
__Block_byref_age_0 age = {
(void*)0, //isa 传0
(__Block_byref_age_0 *)&age, //自己的地址传给__forwarding(它内部有个指针指向自己)
0,
sizeof(__Block_byref_age_0),// 当前结构体多大
10 //age的值为10
};
结构体示意图,如下所示:
调用block的时候,block内部会调用__main_block_func_0函数,可以看出:
__Block_byref_age_0 *age = __cself->age;
(age->__forwarding->age) = 30;
先通过age指针拿到__forwarding指针(里面存的就是自己),再通过__forwarding指针拿到自己里面的值,然后修改值为30。
总结:使用__block修饰age,会将age包装成__Block_byref_age_0结构体(对象),对象里面存着isa,对象的地址,对象的大小,age的值,然后通过对象里面的__forwarding指针拿到自己,再拿到自己的age值,进行修改。如果没修改外面的变量就不要加__block,因为又包装了一层对象,等用到的时候再加。
问题:执行下面代码会报错吗?
NSMutableArray *arr = [NSMutableArray array];
MJBlock block = ^{
[arr addObject:@"123"];
};
回答:不会。因为“ [arr addObject:@"123"];”是使用arr而不是修改它的值(例如:arr = nil)。
Demo地址:block的copy操作和__block