基本使用
block常见的使用方式如下:
// 无参无返回值
void(^MyBlockOne)(void) = ^(void) {
NSLog(@"无参数, 无返回值");
};
MyBlockOne();
// 有参无返回值
void (^MyBlockTwo)(int a) = ^(int a) {
NSLog(@"a = %d", a);
};
MyBlockTwo(10);
//有参有返回值
int (^MyBlockThree)(int, int) = ^(int a, int b) {
NSLog(@"return %d", a + b);
return 10;
};
MyBlockThree(10, 20);
// 无参有返回值
int (^MyBlockFour)(void) = ^(void) {
NSLog(@"return 10");
return 10;
};
// 声明为某种类型
typedef int (^MyBlock) (int, int);
@property (nonatomic, copy) MyBlock block;
Block的本质 - OC对象
结论: block
的内部存在isa
指针,其本质就是封装了函数调用
和函数调用环境
的OC对象
。
证明方法一:底层结构窥探
main
函数中定义一个block
,如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^(void) {
NSLog(@"this is first block");
};
block();
}
return 0;
}
终端进入项目所在目录,通过xcrun
命令将OC代码
转为C++代码
:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
转换结果如下:
// 1. block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block 内部impl结构体,存储isa指针,block方法的地址。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; // 方法地址
};
// block 的描述信息,如:block的大小
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c441779a345740a98accb31ac195d61f~tplv-k3u1fbpfcp-zoom-1.image)static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// 2. block 的方法实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_cf18a7_mi_0);
}
// 3. main方法的实现
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
将生成的main
方法简化后得:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
block->FuncPtr(block);
}
return 0;
}
看简化后的代码,你是不是有疑问, 为什么block->FuncPtr(block)
这句话能调用成功,明明FuncPtr
是__block_impl
类型里的成员,为什么可以直接使用block调用
?
原因其实很简单,因为在block
结构体__main_block_impl_0
内,__block_impl
是第一个成员变量,因此block
的地址和impl
的地址是相同的。两者可以进行强制转换。
根据转换结果:
-
OC
中定义的block
底层其实就是一个C++ 的结构体__main_block_impl_0
。结构体有两个成员变量impl
、Desc
,分别是结构体类型__block_impl
、__main_block_desc_0
。 - 结构体
__block_impl
内包含了isa
指针和指向函数实现的指针FuncPtr
。 - 结构体
__main_block_desc_0
内Block_size
成员存储着Block的大小
。
由上可知,block
内部有一个isa
指针,因此,block本质其实就是一个OC对象
。
证明方法二:代码层面
如果block
是一个OC对象,那它最终肯定继承自NSObject
类(NSProxy除外
),因此我们可以直接打印出block
继承链看一下就知道了。
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^(void) {
NSLog(@"this is first block");
};
NSLog(@"class = %@", [block class]);
NSLog(@"superclass = %@", [block superclass]);
NSLog(@"superclass superclass = %@", [[block superclass] superclass]);
NSLog(@"superclass superclass superclass = %@", [[[block superclass] superclass] superclass]);
}
return 0;
}
输出结果:
2020-07-28 19:25:24.475317+0800 LearningBlock[39445:591948] class = __NSGlobalBlock__
2020-07-28 19:25:24.475707+0800 LearningBlock[39445:591948] superclass = __NSGlobalBlock
2020-07-28 19:25:24.475762+0800 LearningBlock[39445:591948] superclass superclass= NSBlock
2020-07-28 19:25:24.475808+0800 LearningBlock[39445:591948] superclass superclass superclass= NSObject
block
的继承链: __NSGlobalBlock
-> NSBlock
-> NSObject
可以看出block最终继承自NSObject的
。isa指针
其实就是由NSObject
来的。 因此block
本质就是一个OC
对象。
Block 的变量捕获(Capture)
为了保证block
内部能够正常访问外部的值,block
有个变量捕获的机制。下面来一起来探索一下block的变量捕获机制
。
代码:
int a = 10; // 全局变量, 程序运行过程一直存在内存。
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int b = 20; // 局部变量,默认是auto修饰,一般可以不写auto,所在作用域结束后会被销毁。
static int c = 30; // 静态变量,程序运行过程中一直存在内存。
void(^block)(void) = ^(void) {
NSLog(@"a = %d, b = %d, c = %d", a, b, c);
};
// 观察调用block时,a,b,c 的值是多少呢?
a = 11;
b = 21;
c = 31;
block(); // 调用block
}
return 0;
}
打印输出:
2020-07-28 19:43:41.729849+0800 LearningBlock[39648:603167] a = 11, b = 20, c = 31
由打印结果来看,b
没有改变, 而a
和c
的值都发生了变化。 原因是什么呢?下面一起看下
运行下面的转换语句,将当前的OC代码转换C++, 方便我们看到更本质的东西:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
转换后的代码如下:
int a = 10; // 全局变量
struct __main_block_impl_0 { // block的结构体
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int b; // 新生成的成员变量b,用于存放外部局部变量b的值
int *c; // 新生成的成员变量c,指针类型, 用于存储外部静态局部变量c的引用。
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int *_c, int flags=0) : b(_b), c(_c) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int b = __cself->b; // 通过cself进行访问内部的成员变量b
int *c = __cself->c; // 通过cself获取静态局部变量c的引用
// 直接访问全局变量a
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_256a11_mi_0, a , b, (*c));
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int b = 20;
static int c = 30;
void (*Myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b, &c));
a = 11;
b = 21;
c = 31;
((void (*)(__block_impl *))((__block_impl *)Myblock)->FuncPtr)((__block_impl *)Myblock);
}
return 0;
}
有上可以观察到:
block
结构体__main_block_impl_0
内部生成了新的成员变量b
和*c
, 分别用于存放外部传进来的b
和c的地址
,这就是我们所说的捕获
。而对于全局变量a
则没有进行捕获,在使用时是直接访问的。
由此可得出:
-
block
内部对auto
和static
类型的变量进行了捕获
,但是不会捕获全局变量
。 - 虽然block对
auto
和static
变量都进行了捕获,但是不同的是,auto
变量是值传递,而static
变量则是地址传递。因此当外部的static
变量值发生变化时,block
内部也跟着会改变,而外部的auto
变量值发生变化,block
内部的值不会发生改变。
[图片上传失败...(image-3381f6-1601373145542)]
思考
相信你会有这样的疑问,为什么block
会捕获auto
和static
类型的局部变量,而不会捕获全局变量呢?(全局变量
表示不服,block
你怎么搞区别对待呢?), 那么block
的变量捕获究竟有什么讲究呢?
其实是这样的
- 首先对于
auto
类型的局部变量,其生命周期太短了,离开了其所在的作用域后,auto
变量的内存就会被系统回收了,而block
的调用时机是不确定的,如果block
不对它进行捕获,那么当block运行时再访问auto
变量时,因为变量已被系统回收,那么就会出现坏内存访问
或者得到不正确的值
。 - 对于局部的
static
变量,因为其初始化之后,在程序运行过程中就会一直存在内存中,而不会被系统回收,但是由于因为是局部变量的原因,其访问的作用域有限,block想访问它就要知道去哪里访问,所以block才需要对其进行捕获,但与auto
变量不同的是,block
只需捕获static
变量的地址即可。 - 对于
全局变量
,因为其在程序运行过程一直都在,并且其访问作用域也是全局的,所以block
可以直接找到它,而不需要对它进行捕获。
所以,block
的变量捕获原则其实很简单,如果block
内部能直接访问到的变量,那就不捕获(捕获也是浪费空间
), 如果block内部不能直接访问到变量,那就需要进行捕获(不捕获就没得用
)。
Block的类型
block有3种类型,可以通过调用class
方法或者isa指针
查看具体类型,最终都是继承自NSBlock
类型.
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
为了准确分析block
的类型,先把ARC
给关闭,使用MRC
。
[图片上传失败...(image-dd67c0-1601373145542)]
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10; // 局部变量,默认是auto,一般可以不写auto,所处作用域结束后会被销毁。
static int height = 20; // 静态变量,程序运行过程中一直存在内存。
void(^block1)(void) = ^(void) {
NSLog(@"1111111111"); // 没有捕获了auto变量
};
void(^block2)(void) = ^(void) {
NSLog(@"age = %d", age); // 捕获了auto变量
};
void(^block3)(void) = ^(void) {
NSLog(@"height = %d", height); // 捕获了static变量
};
NSLog(@"block1 class: %@", [block1 class]); // __NSGlobalBlock__
NSLog(@"block2 class: %@", [block2 class]); // __NSStackBlock__
NSLog(@"block2 copy class: %@", [[block2 copy] class]); //__NSMallocBlock__
NSLog(@"block3 class: %@", [block3 class]); //__NSGlobalBlock__
}
return 0;
}
// 输出结果:
2020-07-28 20:41:43.283331+0800 LearningBlock[40390:637401] block1 class: __NSGlobalBlock__
2020-07-28 20:41:43.283755+0800 LearningBlock[40390:637401] block2 class: __NSStackBlock__
2020-07-28 20:41:43.283877+0800 LearningBlock[40390:637401] block2 copy class: __NSMallocBlock__
2020-07-28 20:41:43.283924+0800 LearningBlock[40390:637401] block3 class: __NSGlobalBlock__
由上可知:
-
block
类型取值如下:- 没有捕获
auto
变量,那么block
的为__NSGlobalBlock__
类型。 - 捕获了
auto
变量,那么block
为__NSStackBlock__
类型。 - 对
__NSStackBlock__
类型的block
进行copy
操作,则block
就会变成__NSMallocBlock__
类型.
- 没有捕获
-
block
这几种类型的主要区别是:在内存中的存放区域不同。(即生命的周期不同)-
__NSGlobalBlock__
存在数据段。 -
__NSStackBlock__
存放在栈空间。 -
__NSMallocBlock__
存放在堆空间。
-
检验题:
新建一个Person类, 如下:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)test;
@end
@implementation Person
- (void)test {
void (^block)(void) = ^{
NSLog(@"person name = %@", _name);
};
}
@end
问题: 在Person.m
的test方法
中的block
对self
有没有进行捕获呢?
答案是有,block
会捕获self
. 分析如下:
首先将Person.m
通过xcrun
命令转换为C++
, 得到如下内容:
//test 方法内的block方法
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//test 方法
static void _I_Person_test(Person * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_Person_14871d_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));
}
观察转换后的代码可以看到:
- 我们平常写的OC方法,其实默认就有隐藏的两个参数,
(Person *self, SEL _cmd)
, 分别是方法的调用者 self
和方法选择器 sel
。 - 方法的参数一般是局部变量,block会对局部变量进行捕获的。
Block的copy操作
我们日常使用的block一般是__NSMallocBlock__
类型的,原因有如下:
- 对于
__NSGlobalBlock__
类型的block
, 因为没有捕获auto
变量, 所以正常一般都是直接使用函数实现。 - 对于
__NSStackBlock__
类型的block
, 因为其存放在栈上,其内部使用变量容易被系统回收掉,从而导致一些异常的情况。比如下面:(要先将项目切成MRC,因为ARC下编译器会根据情况做copy操作,会影响分析)
typedef void (^MJBlock)(void);
MJBlock block;
void test() {
int a = 10; // test方法结束后,a的内存就被回收了。
block = ^(void) {
NSLog(@"a = %d", a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block(); // block里打印的是被回收了的a
}
return 0;
}
输出结果:
2020-09-27 10:05:28.616920+0800 Interview01-block的copy[7134:29679] a = -272632776
- 对于
__NSMallocBlock__
类型的block
, 因为它是存储在堆上,所以就不存在__NSStackBlock__
类型block
的问题。
上面演示的是在MRC
环境下的, 那么在ARC
环境下又是如何的呢?
在ARC
环境下,编译器会自动根据情况将栈上的block
复制到堆上。比如一下情况:
-
block
作为函数返回值时。 - 将
block
赋值给__strong
指针时。 -
block
作为Cocoa API
中方法名含有usingBlock
的方法参数时。 -
block
作为GCD API
方法参数时。
typedef void (^MJBlock)(void);
MJBlock myblock()
{
int a = 10;
return ^{
NSLog(@"--------- %d", a); // 1. 作为方法返回值时。会自动copy
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MJBlock block = ^{ // 2.赋值给strong指针时,会自动copy
NSLog(@"---------%d", age);
};
NSArray *arr = @[@10, @20];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 3. block作为Cocoa API中方法名含有usingBlock的方法参数时。会自动copy
}];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 4. block作为GCD API方法参数时。会自动copy
});
}
return 0;
}
根据上面的情况,在MRC
和ARC
下block属性
的写法可以有差异:
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void); // 赋值时会自动copy到堆上
ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
对象类型的auto变量
基本数据类型
的auto
变量我们已经分析了,那么对象类型
的auto
变量是不是和基本数据类型的一样还是有什么特别之处呢?下面我们一起来分析下:(记得先将工程切回ARC模式)
如下代码:
@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@end
@implementation LCPerson
- (void)dealloc {
NSLog(@"%s", __func__); // 销毁代码
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
}
NSLog(@"22222222");
}
return 0;
}
// 输出结果:
2020-09-27 10:36:43.856070+0800 LearningBlock[16016:56873] 11111111
2020-09-27 10:36:43.856442+0800 LearningBlock[16016:56873] -[LCPerson dealloc]
2020-09-27 10:36:43.856474+0800 LearningBlock[16016:56873] 22222222
我们定义了一个LCPerson
类,在main.m
中做测试,由输出结果可以看出,person对象的释放是在1111111
和 22222222
之间, 这我们应该都可以理解。(局部作用域)
我们继续~
加入Block之后,我们再观察一下。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
MyBlock block;
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
block = ^(void){
NSLog(@"person age = %d", person.age);
};
}
NSLog(@"22222222");
}
NSLog(@"3333333");
return 0;
}
输出结果:
2020-09-27 10:52:27.578241+0800 LearningBlock[20478:70040] 11111111
2020-09-27 10:52:27.578627+0800 LearningBlock[20478:70040] 22222222
2020-09-27 10:52:27.578688+0800 LearningBlock[20478:70040] -[LCPerson dealloc]
2020-09-27 10:52:27.578729+0800 LearningBlock[20478:70040] 3333333
根据结果,我们可以发现加入了block
之后,person
的销毁是在222222
之后发生的,即person
所在的作用域结束后,person
对象没有立即释放。 那么block
究竟对person
干了什么,导致person对象没能及时释放呢? 为了分析,我们将上面的代码先简化一下。简化如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
void (^block)(void) = ^(void){
NSLog(@"person age = %d", person.age);
};
block();
}
return 0;
}
将上面OC代码转换为C++代码:(支持ARC、指定运行时系统版本)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
转换后的C++代码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
LCPerson *__strong person; // strong类型的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LCPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
LCPerson *__strong person = __cself->person; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_5882d6_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_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) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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;
LCPerson *person = ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
通过观察可以发现,block
内部对person
进行了捕获。并且与捕获基本数据类型的auto
变量不同的是,捕获对象类型时__main_block_desc_0
结构体多了两个函数,分别是copy
和dispose
,这两个函数与被捕获对象的引用计数的处理有关。
- 当
block
从栈
上拷贝到堆
上时,copy
函数被调用,接着它会调用_Block_object_assign
函数,处理被捕获对象的引用计数,如果捕获变量时是使用__strong
修饰,那么对象的引用计数就会+1
. 如果捕获时是__weak
修饰,则引用计数不变。(下面会验证) - 当
block
被回收,即释放时,dispose
函数被调用,接着它会调用_Block_object_dispose
函数,如果捕获变量时是使用__strong
修饰,那么对象的引用计数就会-1
. 如果捕获变量时是__weak
修饰,则引用计数不变。(下面会验证)
我们知道,在ARC
环境下,将block
赋值给__strong
指针,block
会自动调用copy
函数。所以 person
对象离开了局部作用域后没有释放的原因就很明确了,是因为block
调用copy
函数时,将person
对象的引用计数增加了1,所以当局部作用域结束时,person对象
的引用计数并不为0,因此不会释放。 而当block
的作用域结束,block
调用dispose
函数,将person
的引用计数减为0,然后person
才会释放。
如上面所说,那如果是在MRC
环境下,person
对象离开局部作用域后就会销毁了, 因为在MRC
环境下,将block
赋值给__strong
指针是不会触发copy
函数的,所以person
对象应该可以正常释放。
验证一: 将工程切换到MRC
模式下,测试刚才的代码,如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
MyBlock block;
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
block = ^(void){
NSLog(@"person age = %d", person.age);
};
[person release]; // MRC下需要手动管理内存
}
NSLog(@"22222222");
}
NSLog(@"3333333");
return 0;
}
// 输出结果:
2020-09-27 11:39:05.493388+0800 LearningBlock[33422:105156] 11111111
2020-09-27 11:39:05.493800+0800 LearningBlock[33422:105156] -[LCPerson dealloc]
2020-09-27 11:39:05.493833+0800 LearningBlock[33422:105156] 22222222
2020-09-27 11:39:05.493857+0800 LearningBlock[33422:105156] 3333333
观察输出结果,和预料中的一样。person
对象离开局部作用域后正常释放。
验证二: 用weak
修饰的对象类型的auto
变量. (记得切回ARC环境)
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"11111111");
MyBlock block;
{
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
// 弱指针
__weak LCPerson *weakPerson = person;
block = ^(void){
NSLog(@"person age = %d", weakPerson.age);
};
}
NSLog(@"22222222");
}
NSLog(@"3333333");
return 0;
}
// 输出结果:
2020-09-27 12:00:20.461929+0800 LearningBlock[39325:122309] 11111111
2020-09-27 12:00:20.462321+0800 LearningBlock[39325:122309] -[LCPerson dealloc]
2020-09-27 12:00:20.462361+0800 LearningBlock[39325:122309] 22222222
2020-09-27 12:00:20.462391+0800 LearningBlock[39325:122309] 3333333
观察输出结果,和预料中的一样。person
对象离开局部作用域后正常释放。
总结:
-
当
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
)
- 会调用
__block修饰符
-
__block
可以用于解决block
内部无法修改auto变量
值的问题 -
__block
不能修饰全局变量
、静态变量
(static
) - 编译器会将
__block
变量包装成一个对象.
下面一起验证一下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
void (^block)(void) = ^{
a = 20;
NSLog(@"a = %d", a);
};
block();
}
return 0;
}
// 输出结果:
a = 20
将上面OC代码转换为C++代码:(支持ARC、指定运行时系统版本)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
得到转换后结果:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref 这就捕获到的a的引用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20; // 修改值a的值。
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_j2sf07q142992_z55yg_170w0000gp_T_main_ca9eb0_mi_0, (a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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;
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10}; // 这就是__block 修饰的a变量。
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); // 传入的是a变量的地址。
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
由上面可以看到,OC
代码 __block int a = 10
转换为C++
之后变为了:
__Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10};
__Block_byref_a_0
是一个结构体,结构如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
}
所以在OC中用__block
修饰一个变量, 编译器会自动生成一个全新的OC对象。
__block的内存管理
__block
的在block
中的内存管理和对象类型的auto
变量类似(但也有区别)。
当
block
在栈上时,并不会对__block
变量产生强引用-
当
block
被copy
到堆时- 会调用
block
内部的copy
函数 -
copy
函数内部会调用_Block_object_assign
函数 -
_Block_object_assign
函数会对__block
变量形成强引用(retain)。(这点就是和对象类型的auto
变量有区别的地方,对于对象类型的auto
变量,_Block_object_assign
函数会根据auto变量的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作, 而__block
则是直接强引用 )
- 会调用
-
当
block
从堆中移除时- 会调用
block
内部的dispose
函数 -
dispose
函数内部会调用_Block_object_dispose
函数 -
_Block_object_dispose
函数会自动释放引用的__block
变量(release
)
- 会调用
__block的__forwarding指针
被__block修饰的对象类型
通过上面我们知道了用__block
修饰的基本数据类型
的处理。那用__block
修饰的对象类型的处理是不是一样的呢? 下面我们一起看下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
void(^block)(void) = ^(void) {
NSLog(@"person age %d", person.age);
};
block();
}
return 0;
}
通过xcrun
命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
转换成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*); // 管理person的内存
void (*__Block_byref_id_object_dispose)(void*); // 管理person的内存
LCPerson *__strong person; //arc环境下, copy 和 dispose函数,会根据person的修饰类型(__strong、__weak)来对person做相应的内存管理。
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_person_0 *person; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__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_person_0 *person = __cself->person; // bound by ref // 这里就是强引用
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hc_wwwl26516td3w0ds9cx80c280000gp_T_main_213c56_mi_0, ((int (*)(id, SEL))(void *)objc_msgSend)((id)(person->__forwarding->person), sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 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;
__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, ((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LCPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("alloc")), sel_registerName("init"))};
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)(person.__forwarding->person), sel_registerName("setAge:"), 10);
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
当block
从栈
拷贝到堆
上时,会调用block
的copy
方法,同时还会调用__Block_byref_person_0
结构体里的__Block_byref_id_object_copy
方法,__Block_byref_id_object_copy
内部会调用_Block_object_assign
方法,处理结构体__Block_byref_person_0
内部的person
指针所指对象的引用计数。
总结如下:
当
__block
变量在栈上时,不会对指向的对象产生强引用-
当
__block
变量被copy
到堆时- 会调用
__block
变量内部的copy
函数 -
copy
函数内部会调用_Block_object_assign
函数 -
_Block_object_assign
函数会根据所指向对象的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain
)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
)
- 会调用
-
如果
__block
变量从堆上移除- 会调用
__block
变量内部的dispose
函数 -
dispose
函数内部会调用_Block_object_dispose
函数 -
_Block_object_dispose
函数会自动释放指向的对象(release
)
- 会调用
对象类型的 auto变量 和 __block变量处理的异同:
当block在栈上时,对它们都不会产生强引用
-
当block拷贝到堆上时,都会通过copy函数来处理它们
- __block变量(假设变量名叫做a)
- _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
-
对象类型的auto变量(假设变量名叫做p)
- _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
-
当block从堆上移除时,都会通过dispose函数来释放它们
- __block变量(假设变量名叫做a)
- _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
-
对象类型的auto变量(假设变量名叫做p)
- _Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);
循环引用问题
在开发过程中我们经常会遇到block循环引用的问题, 如下:
typedef void (^MyBlock)(void);
@interface LCPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block;
@end
@implementation LCPerson
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person age %d", person.age);
};
NSLog(@"211212121122");
}
return 0;
}
// 输出结果:
2020-09-28 20:01:48.358822+0800 LearningBlock[41115:298402] 211212121122
由打印结果可以看出,person
并没有释放(没有调用person的dealloc方法)。那是什么原因导致的呢?是循环引用。 下面我们来分析一下:
-
@property (nonatomic, copy) MyBlock block;
从这句话可以看出,person
强引用着block
. -
block
内部访问了person
对象的age
属性,根据上面所学,我们知道block
会对person
进行捕获,并且在arc
环境下,block
赋值给__strong
指针时会自动调用copy
方法,将block
从栈拷贝到堆上, 这样会导致person
的引用计数加1,即block
强引用着person
。
所以person
与block
相互强引用着,出现了循环引用,所以person
对象不会释放。
那么该如何解决呢? 下面说下在ARC
环境和MRC
环境分别如何处理?
解决循环引用问题 - ARC
在ARC
环境下,我们可以通过使用关键字__weak
、__unsafe_unretained
来解决。如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
__weak LCPerson *weakPerson = person;
// 或者 __unsafe_unretained LCPerson *weakPerson = person;
person.block = ^{
NSLog(@"person age %d", weakPerson.age);
};
NSLog(@"211212121122");
}
return 0;
}
// 打印结果:
2020-09-28 20:30:19.659679+0800 LearningBlock[41212:307877] 211212121122
2020-09-28 20:30:19.660256+0800 LearningBlock[41212:307877] -[LCPerson dealloc]
示意图如下:
还可以使用__block
解决, 如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person age %d", person.age);
person = nil;
};
person.block(); // 必须调用
NSLog(@"211212121122");
}
return 0;
}
// 打印结果:
2020-09-28 20:35:32.531704+0800 LearningBlock[41256:310297] person age 10
2020-09-28 20:35:32.532221+0800 LearningBlock[41256:310297] -[LCPerson dealloc]
2020-09-28 20:35:32.532310+0800 LearningBlock[41256:310297] 211212121122
使用__block
解决,必须调用block,不然无法将循环引用
打破。
疑问: __weak
和__unsafe_unretained
关键字有什么区别呢?
使用__weak
和__unsafe_unretained
关键字都能达到弱引用的效果。这两者主要的区别在于,使用__weak
关键字修饰的指针,在所指的对象销毁时,指针存储的地址会被清空(即置为nil), 而__unsafe_unretained
则不会。
解决循环引用问题 - MRC
- MRC环境是没有
__weak
关键字的,所以可以使用__unsafe_unretained
关键字解决。(与ARC差不多,这里就不演示了) - 同样也可以是
__block
关键字解决。如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block LCPerson *person = [[LCPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"person age %d", person.age);
person = nil;
};
[person release]; // MRC需要手动添加内存管理代码
NSLog(@"211212121122");
}
return 0;
}
与ARC
不同的是,MRC
下使用__block
解决循环引用问题,不要求一定要调用block
。原因上面__block修饰的对象类型
里有说到:
_Block_object_assign
函数会根据所指向对象的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain
)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
)
后话
这篇文章有点乱,还有待改进。写博客真的费时间,不过能加深印象,也不错。
参考
- MJ 底层原理