iOS Block底层探索
【原创博文,转载请注明出处!】
一、block的本质是什么?
接下来通过一个简单的demo,开启我们探索block之门。
定义一个简单的block并调用:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
^(){
NSLog(@"Hello world, I'm block!");
}();
}
return 0;
}
通过平台指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
,将OC代码转换成C++代码 。
关于怎么将OC代码转换成C++代码,可以参见之前的博客谈谈我对OC本质的理解,里面详细解释了xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
这段指令的含义。
转换后的C++代码如下:
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;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
}
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;
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
}
return 0;
}
对比之下,原来OC下的block相关代码
^(){
NSLog(@"Hello world, I'm block!");
}();
对应的C++代码就是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
通过C++代码发现,block的调用实际上就是__main_block_impl_0这个结构体,结构体实现如下:
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;
}
};
__main_block_impl_0
结构体内部有一个与结构体同名的__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
函数,这是C++结构体的写法,该函数为结构体的构造函数,相当于OC类中的- (instancetype)init;
方法。__main_block_impl_0
函数携带三个参数,最后一个参数为可选的,默认值为0。
再看结构体__main_block_impl_0
,发现其第一个成员imp
也是个结构体,结构体类型为__block_impl
。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
通过结构体__block_impl
实现代码中的isa
指针,显而易见这是个对象,因此可以准确地说block的本质是一个OC对象。(为什么是OC对象?因为OC的基本数据类型没有isa指针这个概念!博主前面的博客中有讲到,可以翻翻)。
结构体的第二个成员仍然是个__main_block_desc_0
类型的结构体
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
该结构体两个成员一个是系统的保留值reserved = 0,另一个Block_size
则代表了该block的大小。
接下来回到block的调用函数__main_block_impl_0
,
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
该函数就是结构体__main_block_impl_0
的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) ,它有两个必传的参数,一个是函数指针fp ,一个是结构体指针desc,关于结构体指针所指向的结构体就是上面分析到的__main_block_desc_0
,那么第一个参数函数指针fp到底是什么?
在这个demo的C++实现代码中,fp指向的函数为__main_block_func_0
,__main_block_func_0
的函数实现代码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
}
由于在OC代码中,我们block体内打印了一个字符串,与这个__main_block_func_0
函数内的代码完全一致。研究发现__main_block_func_0
这个函数的作用就是将block体内的代码封装成一个函数,也就是说block体内的所有OC代码被封装成__main_block_func_0这个函数。与我们OC中的代码NSLog(@"Hello world, I'm block!");
相对应的就是NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
没毛病。
通过上面的分析,可以肯定的是block本质上也是一个OC对象,它内部也有一个isa指针,block还是一个封装了函数的调用的OC对象。
二、block的变量捕获(capture)
A.局部变量之auto变量
什么是auto变量?局部变量有哪几种?
所谓的auto变量就是非静态的局部变量,离开作用于就会销毁。例如下面这个函数:
- (void)example{
int a = 5; //等价于auto int a = 5;
NSString *name = @"Rephontil.Zhou"; //等价于 auto NSString *name = @"Rephontil.Zhou";
static int b = 10; //这个b就不是auto变量
}
常识小结:通常情况下我们定义的局部非static变量都是auto变量,系统会默认在前面加上auto关键字的;但是静态局部变量就不会有auto前缀,加了也会由于报错而编译不通过。
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,这个变量捕获机制又是怎么样呢?我们一点点来探索:
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
};
a = 20;
block();
// 2018-08-28 21:34:33.276996+0800 Interview03-block[99340:9151961] 你好世界!a = : 10 ;
}
return 0;
}
上面demo,block内部访问局部变量a的值,后面在调用block之前修改了a的值,但是打印出来的a的结果仍然为修改之前的值,这与我们的开发经验相符合,但是这究竟是啥原因呢?继续看C++代码的实现:
typedef void(*Block)(void);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
}
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;
int a = 10;
Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
对比之前没有访问任何变量的block结构体,此时的block所对应的结构体__main_block_impl_0里面多了一个成员int a
,并且结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)
多出了一个参数_a
,(知识点:后面的: a(_a)
为C++的语法,意为将参数_a
赋值给成员a
)。
在实现block的时候,对应的C++代码为Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));可见,系统将a作为函数__main_block_impl_0的参数传递进去,所以block所对应的结构体中int a;这个成员所对应的值a = 10;后面我们修改了a的值为20,并使用block();
调用block 打印a的值,这个时候调用了函数__main_block_func_0(struct __main_block_impl_0 *__cself)
,实现如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
}
其内部访问变量a的方式为:int a = __cself->a;__cself
为block所对应的结构体对象,所以这个a也就是之前结构体__main_block_impl_0
中保存的成员变量a的值,即为10,而不是后面修改的20。针对这个问题,我的看法是block在调用的时候,其实此时main()函数中的a变量相对于block来说是个外部的变量,因为block对应的结构体内部有自己的变量a,外面怎么修改不会影响到block结构体内部成员a的值。
B.局部变量之static变量
根据demoA,我们在demoB中中block内部增加访问静态的局部变量static int b以及修改a、b变量的值后,调用block打印的结果:
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto NSString *name = @"周勇";
int a = 10;
static int b = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
NSLog(@"你好世界!b = : %d ;",b);
};
a = 20;
b = 20;
block();
// 2018-08-28 23:16:53.244791+0800 Interview03-block[861:9731638] 你好世界!a = : 10 ;
// 2018-08-28 23:16:53.245153+0800 Interview03-block[861:9731638] 你好世界!b = : 20 ;
}
return 0;
}
发现局部静态变量b修改之后,block内部打印的结果也变了Σ(⊙⊙"a!
局部变量a的访问过程demoA已经分析过了,接下来仍旧通过C++代码研究局部静态变量b的捕获过程:
typedef void(*Block)(void);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_0,a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_1,(*b));
}
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;
int a = 10;
static int b = 10;
Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));
a = 20;
b = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
通过C++代码发现,局部自动变量a与静态变量b的捕获方式不同,block结构体中,a为int
变量,b为int *
变量,也就是指针。在定义block的时候, Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b)),传递的也是b变量的指针,调用block的时候,__main_block_func_0
中获取b也是通过block的结构体__main_block_impl_0 访问内部成员变量b,与结构体外部变量b指向的是同一块内存地址,所以只要有地方修改b,结构体内部也会跟随变化,这样就解释了为啥“同样修改了局部auto变量与局部static变量,block访问的结果不同”。
总而言之:在block内部访问的auto变量为值传递,局部静态变量为引用传递(也就是传递变量的指针)。
C.全局变量
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int age_ = 25;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
static int b = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
NSLog(@"你好世界!b = : %d ;",b);
NSLog(@"你好世界!age_ = : %d ;",age_);
};
a = 20;
b = 20;
age_ = 26;
block();
// 2018-08-29 00:54:13.318712+0800 Interview03-block[2155:10283110] 你好世界!a = : 10 ;
// 2018-08-29 00:54:13.319099+0800 Interview03-block[2155:10283110] 你好世界!b = : 20 ;
// 2018-08-29 00:54:13.319130+0800 Interview03-block[2155:10283110] 你好世界!age_ = : 26 ;
}
return 0;
}
block内部访问全局变量age_,其变化同静态局部变量一样。同样转换成C++代码分析:
typedef void(*Block)(void);
int age_ = 25;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_0,a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_1,(*b));
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_2,age_);
}
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;
int a = 10;
static int b = 10;
Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));
a = 20;
b = 20;
age_ = 26;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
较demoA、demoB不同的是,block结构体内部没有定义age_
变量,block内部访问age_变量的时候,传入的也是全局的age_,因此在任何地方改变这个全局变量,block访问的时候都是这个全局变量的最新值。
通过demoA\B\C,可以肯定对于局部auto变量、static变量、全局变量,block的变量捕获情况如下:
分析了block对自动变量,static变量与全局变量的捕获方式的不同,我认为合理的解释是:自动变量,内存可能会销毁,将来执行block的时候,访问变量的内存,可能会因为不存在引发坏内存访问。
静态局部变量:static变量内存一直会保存在内存中,所以可以取它的最新值,也就是通过指针去取。
三、block的类型有哪几种?
block有3种类型,可以通过调用class方法或者isa指针查看具体类型如下:
NSGlobalBlock ( _NSConcreteGlobalBlock )
NSStackBlock ( _NSConcreteStackBlock )
NSMallocBlock ( _NSConcreteMallocBlock )
这三种类型最终都是继承自NSBlock类型。
通过关键字可以知道这三种类型block分别存放在内存的全局区、栈区、堆区,在内存中对应的区域图示如下:
- 程序区域存放的就是我们写的代码,比如一个Person类里面的代码。
- 数据区也就全局区,存放着程序中使用到的全局变量。
- 堆存放的就是我们新建的对象。如[[Person alloc] init]出来的,这部分内存需要我们手动释放。
- 栈区存放的就是自动变量,一般在函数调用之后,这些自动变量所占用内存也就被系统回收了。
由于在ARC环境下,编译器为我们做了很多额外的工作,比如将栈区的block copy到堆区,我们在ARC下也就不容易捕获到block初始状态的位置。所以暂时将开发环境切换至MRC下:
切换环境至MRC.jpg在MRC下,定义两个block,一个访问auto变量,一个不访问auto变量,最后对访问auto变量的block调用copy方法,依次查看三种情况下block所对应的类型如下:
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
int age_ = 25;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
static int b = 10;
Block block = ^(){
NSLog(@"你好世界!a = : %d ;",a);
};
Block block1 = ^(){
NSLog(@"你好世界");
};
NSLog(@"%@ %@ %@",[block class],[block1 class],[[block copy] class]);
// __NSStackBlock__ __NSGlobalBlock__ __NSMallocBlock__
}
return 0;
}
访问了auto变量的block在栈区,不访问auto变量的block在全局区。对栈区的block调用copy方法,block居然移到了堆区!后面我们对全局区的block调用copy,发现全局区域的block仍旧在全局区。
三种类型block产生的原因每一种类型的block调用copy后的结果如下所示:
对各种block进行copy后的block内存区域变化四、block的copy
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
- block作为函数返回值时;
- 将block赋值给__strong指针时;
- block作为Cocoa API中方法名含有usingBlock的方法参数时;
- block作为GCD API的方法参数时。
MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
为什么要对block进行copy操作?
假如在MRC环境下,在某个函数内定义了一个block变量,并在block中访问了局部变量,但是并没有立即调用该block。后面等到调用该函数的时候,再调用block,看下面的demo:
调用block后,block内部访问的局部变量打印的结果很糟糕,程序倒是没奔溃,但是结果不如人所愿。
出现这种情况的原因很好理解:由于这个block访问了auto变量,因此是一个NSStackBlock
类型的block,该block对应的结构体分配在栈内存上,等到test()函数调用完毕,栈内存会被回收,所以block被调用的时候,访问block结构体内部的变量a,a所对应的内存区域随时可能被系统回收,其内存上的数据也是不确定的。
这种情况该如何保证我们调用block的时候,还能正常访问局部变量呢?正如前面列出的,调用copy
方法将block从栈区copy到堆区,事情就解决了。【当然,换成ARC环境,我们通常在声明block属性的时候,使用copy 或 strong关键词修饰,系统也会自动帮我们将block从栈区拷贝到堆区。也就无需我们动手调用block的copy
方法了。但是系统底层还是帮我们对block做了copy
操作】。
"copy"这个操作在ARC下是没有必要的。由于我们的block赋值给了void(^block)(void),这个变量默认是__strong
修饰的,满足编译器会根据情况自动将栈上的block复制到堆上的条件2,即"将block赋值给__strong指针时"。
block访问对象类型的auto变量
前面demo中,block访问的都是基本类型的变量。现在我们换对象类型变量看看有啥不同(⊙_⊙)?
block访问对象类型auto变量.png访问对象类型auto变量,转换后的C++代码如下:
void(*block)(void);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
TestClass *__strong testClass;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, TestClass *__strong _testClass, int flags=0) : testClass(_testClass) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
TestClass *__strong testClass = __cself->testClass; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_c36270_mi_0,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)testClass, 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->testClass, (void*)src->testClass, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->testClass, 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;
TestClass *testClass = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)testClass, sel_registerName("setAge:"), (NSInteger)20);
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, testClass, 570425344));
((void (*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)testClass, sel_registerName("setAge:"), (NSInteger)25);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
对比访问基本数据类型:
相同点:block结构体内部也存在一个与被访问的变量同名的成员变量,本demo中,也就是TestClass *__strong testClass;
即testClass实例。
不同点:__main_block_desc_0
结构体的实现,发现其内部增加了两个函数:
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)
;
void (*dispose)(struct __main_block_impl_0*)
;
关于copy函数与dispose函数的调用机制:
如果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)。
所谓的弱引用就是在变量前面加上“__weak”修饰符,默认是"__strong"修饰的,即默认对变量为强引用。创建testClass实例加上“__weak”前缀,__weak __weak TestClass *weakTestClass = testClass;对应C++代码中,block结构体内部的成员testClass就声明成 TestClass *__weak weakTestClass;
在使用clang将OC代码转换为C++代码时,可能会遇到以下问题:
cannot create __weak reference in file using manual reference
解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
MRC下没有"强引用"的概念,只有retain、release概念。栈空间block不会保住引用对象的命,即不会对引用对象进行强引用。但是堆空间block通过强引用被访问对象,从而延长被引用对象的生命周期。
所以可以对栈内存的block进行copy操作,变成堆内存上的block,这样被block引用的对象生命周期就会保住,等到block销毁的时候,被引用的对象才会销毁。
不管ARC、MRC,栈空间block都不会持有对象,如果是堆空间block,有能力保住被引用对象的命,换成ARC下的说法就是“强引用”,MRC下没有“强引用”。
在ARC下,我们通过demoA、demoB看看以往开发中,在堆上block内访问oc对象使用不同修饰符造成的结果:
demoA:
demoA - block弱引用OC对象.pngdemoB:
demoB - block强引用OC对象..pngdemoB中,testClass对象默认被__strong
修饰符修饰,block会对其强引用,转换为C++环境后block结构体内容如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
TestClass *__strong testClass;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, TestClass *__strong _testClass, int flags=0) : testClass(_testClass) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
block结构体内成员变量testClass
就是就是OC中alloc出来的testClass对象,由于该block在堆上,在当前情况下,只要该block没有被销毁,block的内存就一直存在,其内部成员变量testClass
由于被"__strong"强引用修饰,也会一直存在,故而TestClass
类也一直存在于内存中,这在很多情况下会成为App内存泄露的根源。
五、__block修饰符
在block函数体里面修改变量在日常开发中常见,我们可以轻松在block体内部修改static变量或全局变量,但是却无法修改auto变量。尝试在block中修改auto变量,编译器错误如下:
block内修改auto变量编译不通过.png想必入门级的iOS开发者都知道怎么解决这个问题。这里我们起码有三种解决方案:
①、将需要修改的变量设置为全局的;
②、将需要修改的变量设置为static类型;
③、在需要修改的变量前加上“__block
”修饰符。
前两种方案一般技术开发都不会采用☹️。
针对方案①,全局变量定义太多影响代码阅读,全局变量生命周期长,占内存;
针对方案②,static类型变量的生命周期同APP一样长,一直存在于内存中,所以舍弃。
方案③是完美方案,“__block
”能解决开发中99%以上的问题,1%是例外,后面讲(^^)。
通过demo,我们看看“__block
”做了啥(⊙_⊙)?
OC转换为C++代码:
void(*block)(void);
struct __Block_byref_age_0 {
void *__isa;
__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;
__Block_byref_age_0 *age; // by ref
__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->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_3fd458_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, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}
w(゚Д゚)w,变了。。。
__Block_byref_age_0
这种类型的结构体本文第一次出现,根据其内部定义:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
内部包含isa指针,毫无疑问是一个OC对象;
成员__forwarding
指向结构体本身,这个操作也很骚,苹果套路深;
成员__size
保存了结构体__Block_byref_age_0
本身的内存大小;
居然有个成员int age,并且由之前老巢__main_block_impl_0
迁移到此;
再看看__main_block_impl_0
结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__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;
}
};
内部也有一个age成员,不过是结构体__Block_byref_age_0
类型指针,所以block内部访问的auto变量就是该结构体age指针指向的内容。经过“__block”修饰之后,基本类型的“age”变量被包装成“ __Block_byref_age_0”结构体对象。
在main()函数中,我们看到"__block int age = 10;"被定义成:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,
(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10};
同时block被定义成:
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
也就是将"__Block_byref_age_0 "类型的“age”传入到block的构造函数__main_block_impl_0中,因此__main_block_impl_0中的age被赋值这样的结构体:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,
(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10};
即:__Block_byref_age_0中最后一个成员“int age;”被赋值10,__forwarding被赋予__Block_byref_age_0型age的地址,这里就是结构体本身,与前面讲述的一致。
再看看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__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_3fd458_mi_0,(age->__forwarding->age));
}
OC代码age = 20;
对应的C++代码(age->__forwarding->age) = 20;
,C++代码中有两个不同类型的age变量,前一个age是__Block_byref_age_0
类型的结构体指针,后一个age是__Block_byref_age_0
结构体中的int型age变量。中间的__forwarding
指向的还是__Block_byref_age_0
类型的结构体指针age,即第一个age,具体为什么这么设计,后文会讲到。
兜了这么一圈,终于明白了,原来"__block"将局部变量包装成一个__Block_byref_age_0
结构体对象,结构体中有与局部变量同名同类型的变量。在block体内修改"__block"变量,通过一系列指针指向关系,最终指向了__Block_byref_age_0
结构体内与局部变量同名同类型的那个成员,并成功修改变量值。
六、关于__Block_byref_age_0中的__forwarding指针
6-1.png 6-2.png用"__block"修饰auto变量xxx的时候,系统会将这个auto变量xxx转换成一个__Block_byref_xxx_0
结构体类型,结构体中有个成员__forwarding
。当block在栈区的时候,__forwarding
指向栈区的__Block_byref_xxx_0
结构体本身内存地址;当block被copy到堆区的时候,栈上block变量内的__forwarding
将会指向堆上的block变量,从而进一步访问block变量内部的成员。这样,前文中访问age
的时候通过" (age->__forwarding->age) = 20;"
这种做法也就明白了。
七、关于block循环引用及解决方案
常见循环引用示例.pngdemo中,TestClass有一个block实例对象,self对block的关系为强持有。block实现中,也引用了当前实例self,并且也为强引用。这样一来,self持有block,block持有self,所以两者都无法释放,就造成内存泄露。将该.m文件转换为C++实现,看看block结构体__TestClass__test_block_impl_0
和block代码块函数__TestClass__test_block_func_0
:
struct __TestClass__test_block_impl_0 {
struct __block_impl impl;
struct __TestClass__test_block_desc_0* Desc;
TestClass *const __strong self;
__TestClass__test_block_impl_0(void *fp, struct __TestClass__test_block_desc_0 *desc, TestClass *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestClass__test_block_func_0(struct __TestClass__test_block_impl_0 *__cself) {
TestClass *const __strong self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_TestClass_e9b143_mi_0, ((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
}
正如上面分析的一样:由"TestClass *const __strong self;"
可见block结构体中成员变量self为当前类实例的强指针;并且block的代码块中TestClass *const __strong self = __cself->self; // bound by copy
也强引用着当前类TestClass的实例,block与self的关系可以用一张引用自《Objective-C高级编程iOS与OS X多线程和内存管理》中经典图示意:
如何打破block与持有对象间的强引用关系?
在ARC环境下有以下三种解决方案:
- ① 使用
"__weak"
; - ② 使用
"__unsafe_unretained"
; - ③ 使用
"__block"
(必须要调用block)。
对于方案①,几乎所有的开发者都熟悉并应用于项目中,这也是最推荐的方案。那么方案②、③又是什么呢❓其实在我没有研究block底层结构、没有去查阅资料的时候,我也仅仅知道方案①☹️(当然事实也证明②、③方案也不推荐使用)
方案②:“__unsafe_unretained”字面理解就是不安全的、不会导致引用计数增加。简单说就是:不安全的弱引用。
“__weak”与“ __unsafe_unretained”对比:
"__weak":不会产生强引用,当指向的对象销毁时,会自动让指针置为nil;
“ __unsafe_unretained”:不会产生强引用,不安全。当指向的对象销毁时,指针存储的地址值不变,这个时候指向的是一块已经被系统回收的内存,这个时候继续访问会引发"野指针异常"。
对于方案③demo:
#import <Foundation/Foundation.h>
#import "TestClass.h"
@implementation TestClass
- (void)test{
self.age = 20;
// __unsafe_unretained TestClass *weakself = self;
// __weak TestClass *weakself = self;
__block TestClass* weakSelf = self;
self.block = ^{
NSLog(@"%ld", weakSelf.age);
weakSelf = nil;
};
self.block();
}
- (void)dealloc{
NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}
当testClass实例销毁的时候,block也释放了,不会循环引用。
我们分析一下转换后的C++代码:
struct __TestClass__test_block_impl_0 {
struct __block_impl impl;
struct __TestClass__test_block_desc_0* Desc;
__Block_byref_weakSelf_0 *weakSelf; // by ref
__TestClass__test_block_impl_0(void *fp, struct __TestClass__test_block_desc_0 *desc, __Block_byref_weakSelf_0 *_weakSelf, int flags=0) : weakSelf(_weakSelf->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __Block_byref_weakSelf_0 {
void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
TestClass *__strong weakSelf;
};
首先block结构体内持有__block类型的"__Block_byref_weakSelf_0"对象;
其次“__block”类型对象中“TestClass *__strong weakSelf;”,即持有TestClass实例。
由于self持有了block,所以当前对象self、block已经__block变量三者的关系为:
相互持有状态.png
如此一来:又是一个循环引用问题,我们尝试在block代码块内部去掉"weakSelf = nil",实际结果是TestClass实例不会释放掉。针对这种状况,打破三者之间的循环链即可消除循环引用,首先a.对象(也就是持有block的对象)对block的持有关系肯定是强持有,其次b.block对block变量也是强持有的关系,这两条线无法改动!如果突破__block变量持有对象这条线,就可以了,这样就可以通过调用block后,手动设置__block对象为nil。
在本demo中__block变量定义如下:
struct __Block_byref_weakSelf_0 {
void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
TestClass *__strong weakSelf;
};
也就是将结构体__Block_byref_weakSelf_0中的成员变量"TestClass *__strong weakSelf"
置为nil
,即weakSelf = nil
,这样__block就不会持有当前类的实例了,所以循环被打破。打破后三者关系见下图:
由此针对方案③:该方案唯一的缺点就是需要执行block。这么麻烦的关键在于:执行完block之后,在block体内设置引用对象为nil,从而达到手动将__block变量内部的关键成员置为nil
,这样就可以打破循环关系,豁然开朗(^^)。
同时MRC环境下开发,方案有两种:
- ① 用__unsafe_unretained解决;
- ② 用__block解决(block可以不调用)。
“ __unsafe_unretained”同ARC一致;
在使用"_block"时,我们先总结一下block被copy到堆上时,底层做了啥(⊙⊙)?
-
当__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)。
正是因为MRC环境下,__block变量对所引用的对象为弱引用关系,所以“对象”、“block”与"block变量"三种之间处于开环状态,也就不存在循环引用问题,因此在MRC下用__block修饰被引用对象,block可以不调用。正如下面demo:
// ATTENTION:MRC环境
#import <Foundation/Foundation.h>
#import "TestClass.h"
@implementation TestClass
- (void)test{
self.age = 20;
__block TestClass* weakSelf = self;
self.block = ^{
NSLog(@"%ld", weakSelf.age);
};
}
- (void)dealloc{
[super dealloc];
NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
{
TestClass *testClass = [[TestClass alloc] init];
[testClass test];
[testClass release];
}
NSLog(@"---------");
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//2018-08-31 11:25:27.800865+0800 Block[74957:7622148] TestClass - dealloc
//2018-08-31 11:25:27.803116+0800 Block[74957:7622148] ---------
结果是TestClass实例被销毁的时候,block也一起销毁了。
八、__weak搭配__strong使用
__weak TestClass* weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%ld", strongSelf.age);
};
在本demo中,在block内部重新使用__strong修饰weakSelf(被引用)变量是为了在block内部有一个强指针指向weakSelf(弱引用)避免在block调用的时候weakSelf已经被销毁。有些时候block内部访问的对象并不是当前类的实例,考虑到block可能很久才会销毁,因此被block引用的对象应该是弱引用,否则可能造成被引用对象毫无意义地存在于内存中。既然是弱引用,一旦该对象在其他地方被销毁,则block内部的弱引用对象也就销毁了,继续访问也就会返回null
,还是用demo说话吧:
// 1. 新建一个Dog类,并实现dealloc方法;
@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Dog
- (void)dealloc{
NSLog(@"Dog - %@",NSStringFromSelector(_cmd));
}
// 2. 在另一个类的block函数体类访问dog的成员属性name;
#import <Foundation/Foundation.h>
#import "TestClass.h"
#import "Dog.h"
@implementation TestClass
- (void)test{
Dog *dog = [[Dog alloc] init];
dog.name = @"小黑";
__weak Dog *weakDog = dog;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始执行block");
// __strong typeof(weakDog) strongDog = weakDog;
sleep(2);
NSLog(@"狗的名字:%@",weakDog.name);
});
sleep(1);
NSLog(@"模拟weakDog被释放");
dog = nil;
/*
2018-09-04 14:43:32.966624+0800 Block[80253:7808973] 开始执行block
2018-09-04 14:43:33.956807+0800 Block[80253:7808911] 模拟weakDog被释放
2018-09-04 14:43:33.957256+0800 Block[80253:7808911] Dog - dealloc
2018-09-04 14:43:34.972230+0800 Block[80253:7808973] 狗的名字:(null)
*/
}
- (void)dealloc{
NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}
@end
}
- (void)dealloc{
NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}
@end
模拟block内部访问的对象在外部被提前释放的情况,我在调用block的过程中特意将dog设置为nil
,访问的结果是:“Block[76269:7674002] 狗的名字:(null)”,项目中block调用的时机是不确定的,被访问的对象何时候释放也是不确定的,故而这种情况下仅仅使用__weak修饰被访问对象肯定存在问题,为了更好解决这样的问题,我们用“__strong”修饰符在block内部搭配外部的"__weak"修饰被访问对象,针对上面demo,正确的做法如下:
- (void)test{
Dog *dog = [[Dog alloc] init];
dog.name = @"小黑";
__weak Dog *weakDog = dog;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始执行block");
__strong typeof(weakDog) strongDog = weakDog;
sleep(2);
NSLog(@"狗的名字:%@",strongDog.name);
});
sleep(1);
NSLog(@"模拟weakDog被释放");
dog = nil;
/*
2018-09-04 14:46:32.969188+0800 Block[80345:7811829] 开始执行block
2018-09-04 14:46:33.961744+0800 Block[80345:7811757] 模拟weakDog被释放
2018-09-04 14:46:33.962013+0800 Block[80345:7811757] ---------
2018-09-04 14:46:34.974592+0800 Block[80345:7811829] 狗的名字:小黑
2018-09-04 14:46:34.974973+0800 Block[80345:7811829] Dog - dealloc
*/
}
在block内将weakDog对象强引用为strongDog,执行block过程中将dog设置为nil,结果仍能继续访问。
iOS Block底层探索