上篇文章只是简单讲了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变量
1. 通过例子引出结论
① 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变量时(这时就是__NSStackBlock__,放在栈区)
- 如果block是在栈上,将不会对auto变量产生强引用
- 如果栈上的block被拷贝到堆上(自己拷贝的或者ARC下系统自动拷贝的)
会调用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)
2. 验证结论
下面将代码转成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函数的地址。为什么当捕获的是个对象就会多着两个函数呢?这也比较容易理解,既然捕获了对象,就要有内存管理相关了,所以这两个函数就需要了。这两个函数的作用可看上面注释,验证了我们上面的大总结:
3. 小题目
下面用几个小题目测试p什么时候释放。
- 案例一
- (void)touchesBegan:(NSSet<UITouch *> *)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] -------<MJPerson: 0x600002a43e50>
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<UITouch *> *)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-------<MJPerson: 0x600001960630>
2019-12-02 09:12:07 MJPerson - dealloc
2019-12-02 09:12:09 2-------(null)
因为外面的block捕获了p,并且是强引用,所以p会在外面的block执行完毕释放,所以是1s后。里面的block捕获了weakP,但是因为是使用__weak修饰的,所以对象并不会retain,1s后,对象被释放掉,由于weakP和p指向的是同一个对象,所以再过2s后打印是null。
- 案例三
- (void)touchesBegan:(NSSet<UITouch *> *)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-------<MJPerson: 0x600003806760>
2019-12-02 09:13:32 2-------<MJPerson: 0x600003806760>
2019-12-02 09:13:32 MJPerson - dealloc
三. __block修饰变量
先看一个小案例:
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//__block int age = 20;
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还是改不了啊。
1. 那如何才能改?
① 使用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变量包装成一个对象。
- __block不能修饰全局变量、静态变量(static)(因为__block的作用就是上句👆)。
使用__block修饰“__block int age = 20;”,就能在block内部修改外部变量的值,而且不会修改变量的性质(还是auto变量)。
2. 为什么__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)。
__block小疑问:
可能你还有一个疑问,使用__block修饰变量的确可以达到修改变量的值的目的,如果要再次访问变量,到底访问的是__Block_byref_age_0结构体还是结构体里面的“int age”呢?
为了找出答案,我们把block强转成底层实现,代码如下:
#import <Foundation/Foundation.h>
typedef void (^MJBlock) (void);
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
MJBlock block = ^{
age = 20;
NSLog(@"里面访问age is %p", &age);
};
//将block转成block本质
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
block();
NSLog(@"外面访问%p", &age);
}
return 0;
}
在NSLog下面的“}”打断点,如下:
可以打印出外面的age结构体的地址,但是里面的age地址却无法打印。
我们知道,结构体的地址值就是结构体第一个成员的地址值,所以我们可以分析内存计算出里面age的地址,再和打印的地址作比较,计算过程如下:
//结构体地址:0x0000000102808ff0
struct __Block_byref_age_0 {
void *__isa; // 指针占8字节 第一个成员地址:0x0000000102808ff0 (加8算出下个地址)
__Block_byref_age_0 *__forwarding; // 8 0x0000000102808ff8 (加8算出下个地址)
int __flags; // int占4字节 0x0000000102809000 (加4算出下个地址)
int __size; // 4 0x0000000102809004 (加4算出下个地址)
int age; // 0x0000000102809008 可以看出和打印的地址一样
};
打印结果如下:
里面访问age is 0x102809008
Printing description of blockImpl->age:
(__Block_byref_age_0 *) age = 0x0000000102808ff0
外面访问0x102809008
可以看出,计算出的里面的age的地址和我们打印的age的地址是一样的,而且无论是在block里面还是外面访问的都是里面的age。
当然你也可以直接打印出来,都可以验证。
lldb) p/x blockImpl->age
(__Block_byref_age_0 *) $0 = 0x0000000102808ff0
2019-12-02 14:44:26.701568+0800 Interview01-__block[71657:6467841] 0x102809008
(lldb) p/x &(blockImpl->age->age)
(int *) $2 = 0x0000000102809008
解答了我们的疑问:使用__block修饰变量,再次访问,访问的是__Block_byref_age_0里面的int age变量。
现在想一下,为什么苹果要设计成访问变量(使用了__block修饰)直接访问的就是__Block_byref_age_0里面的变量呢?
因为对于一般的开发者来说,可能不知道被__block修饰后还会包装一次,就像KVO苹果重写class方法一样,是不让开发者知道有这么个操作,所以你访问变量,就把里面那个变量的地址给你了。
Demo地址:block的copy操作和__block