主要内容:
1.分析Block捕获外部变量的过程
2.理解Block修改外部变量的限制
3.分析__block存储域类说明符的原理
4.理解__block变量的存储域
5.探究Block对对象的捕获过程
6.Block的循环引用问题
一、分析Block捕获外部变量的过程
为了保证block内部能够正常访问外部的变量,Block有一个变量捕获机制,即Block语法表达式所使用变量可以被保存到Block的结构体实例(Block自身)中。
关于捕获,Block对不同的外部变量的处理有所不同,根据OC中使用变量的分类,大概包括以下几种情况:
- 函数参数(这里研究Block捕获,所以此处不涉及)
- 自动变量(常简称,局部变量)
- 静态局部变量(常简称,静态变量)
- 静态全局变量
- 全局变量
那么,现在对Block捕获外部变量的四种情况进行测试,相关代码如下:
#import <Foundation/Foundation.h>
//使用如下的命令,可将OC代码编译为C++代码
//clang -rewrite-objc main.m
int global_val = 1; //全局变量
static int static_global_val = 1; //静态全局变量
int main(int argc, char * argv[]) {
int val = 1; //自动变量
static int static_val = 1; //局部静态变量
void (^myBlock)(void) = ^{
global_val ++;
static_global_val ++;
static_val ++;
//val++//直接修改会报错(Variable is not assignable (missing __block type specifier)
NSLog(@"\nBlock内:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
};
global_val ++;
static_global_val ++;
val ++;
static_val ++;
NSLog(@"\nBlock外:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
myBlock();
return 0;
}
运行的结果如下:
Block外:
global_val = 2,
static_global_val = 2,
val = 2,
static_val= 2
Block内:
global_val = 3,
static_global_val = 3,
val = 1,
static_val= 3
观察代码运行结果,我们会发现四种情况下,只有静态局部变量、静态全局变量、全局变量可以在Block里被修改,而且直接修改自动变量就会报错;所以此时需要考虑以下两个问题:
1.为什么在Block里不允许更改自动变量?
2.Block捕获不同的变量并修改时,有什么区别吗?
现在将上述代码转化为C++源码来具体分析,转换后的代码如下:
int global_val = 1;
static int static_global_val = 1;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val; //对应静态局部变量
int val; //对应自动变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
int val = __cself->val; // bound by copy
global_val ++;
static_global_val ++;
(*static_val) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_0,global_val,static_global_val,val,(*static_val));
}
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, char * argv[]) {
int val = 1;
static int static_val = 1;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val, val));
global_val ++;
static_global_val ++;
val ++;
static_val ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_1,global_val,static_global_val,val,static_val);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
在代码分析之前,我们有必要对程序中的内存区域划分有所了解,其大致的分类如下:
内存区域 | 具体说明 |
---|---|
栈区 | 存放局部变量的值,系统自动分配和释放; 特点:容量小,速度快,有序 |
堆区 | 存放通过malloc系列函数或new操作符分配的内存,如对象; 一般由程序员分配和释放,如果不释放,则出现内存泄露; 特点:容量大,速度慢,无序; |
静态区 | 存放全局变量和静态变量(包括静态局部变量和静态全局变量); 当程序结束时,系统回收; |
常量区 | 存放常量的内存区域; 程序结束时,系统回收; |
代码区 | 存放二进制代码的区域 |
了解了这些之后,我们再来具体分析代码和执行结果:
1.全局变量和静态全局变量
这两种变量都存储在静态区,在任何时候都可以访问,所以Block无所谓捕获,而是采用了直接访问的方式成功的修改了它们的值;这一点从Block对应的构造函数中就可以看出来:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int _var, int flags=0) : static_var(_static_var), var(_var);
我们看到,用于创建Block的构造函数使用到了静态局部变量和自动变量作为参数,并没有涉及到全局变量和静态全局变量。而且我们也在Block的结构体中只发现了对应的静态变量和自动变量的属性,这进一步说明Block是直接使用全局变量和静态全局变量,而非捕获;
int *static_val; //对应静态局部变量
int val; //对应自动变量
2.自动变量与静态局部变量
虽然自动变量与静态局部变量都被Block捕获,但是只有静态局部变量才可以被修改成功;通过Block中对应的函数__main_block_func_0
,可以观察到Block对外部变量的修改过程,相关代码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_var = __cself->static_var; // bound by copy
int var = __cself->var; // bound by copy
global_var ++;
static_global_var ++;
(*static_var) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_TestBlock_b539f1_mi_0,global_var,static_global_var,var,(*static_var));
}
我们发现,Block为了访问到对应的自动变量和静态局部变量都使用了__cself
,这些操作其实都是针对Block自身属性的,但不同的是:
外部静态局部变量,由于是指针传递,所以修改的是同一个变量,可以修改成功;
外部自动变量,由于是值传递,所以即使修改成功,也无法改变外部自动变量的值;
因此,也许是出于安全的目的,在编译阶段我们就会收到错误提示:Block不能修改其捕获的外部自动变量,即:
Variable is not assignable(missing __block type specifier)
这里还有两个问题值得我们思考:
1.为什么静态局部变量的存储域也在静态区,却不可以像全局变量一样直接修改呢?
关键原因还是"局部"两个字,我们看到C++代码中的函数__main_block_func_0
被设置在了包含Block语法的函数(main
函数,静态局部变量在此处声明定义)之外,所以__main_block_func_0
和静态局部变量和作用域不同,自然不能像全局变量一样随时访问它,所以采用捕获和指针传递的方式来修改静态变量;
2.为什么自动变量不能像静态变量一样指针传递呢?
其实,这主要还是因为自动变量和静态变量的存储域的不同,自动变量存在栈上被销毁的时间不定,这很有可能导致Block执行的时候自动变量已经被销毁,那么此时访问被销毁的地址就会产生野指针错误。
二、理解Block修改外部变量的限制
通过以上的代码示例,我们可以将Block修改外部变量成功的情况分为两种:
第一种:Block直接访问全局性的变量,如全局变量、静态全局变量;
第二种:Block间接访问静态局部变量,捕获外部变量并使用指针传递的方式;
Block中不允许修改外部变量的值的问题,变成了不允许修改自动变量的值的问题;但这也并非最终答案,其实最根本的原因还是Block不允许修改栈中指针的内容;
下面的一段代码,可以从侧面来验证我们的想法:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
NSMutableString *mStr = @"mStr".mutableCopy;
void (^myBlock)(void) = ^{
//mStr = @"newMstr".mutableCopy; //代码1:直接修改了mStr指针内容;
[mStr appendString:@"-ExtraStr"]; //代码2:修改mStr指向的堆中内容;
NSLog(@"Block内:mStr:%@",mStr);
};
NSLog(@"Block外:%@",mStr);
myBlock();
return 0;
}
//打印结果:
//Block外:mStr
//Block内:mStr:mStr-ExtraStr
上述代码是操作一个自动变量的可变字符串,经过测试mStr
不可以直接赋值,却可以通过appendString
修改字符串,这其中的原因是什么呢?
首先还是将代码转化为C++源码,具体如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableString *mStr;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_mStr, int flags=0) : mStr(_mStr) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
NSMutableString *mStr = __cself->mStr; // bound by copy
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)mStr, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_2,mStr);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mStr, (void*)src->mStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mStr, 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[]) {
NSMutableString *mStr = ((id (*)(id, SEL))(void *)objc_msgSend)((id)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_0, sel_registerName("mutableCopy"));
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mStr, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_3,mStr);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
作为对象的字符串会涉及到释放的问题,所以此处转换后的源码与基本类型有所区别(但不影响此处分析,后续会讲到),我们发现Block捕获了mStr
,而且采用了指针传递的方式,这与上面的静态局部变量被捕获的方式很相似,但是mStr
依然不可以直接赋值新的字符串,其实弄清楚问题的关键是理解下面这句代码究做了什么?
mStr = @"newMstr".mutableCopy;
这句代码的含义可以归纳为:@"mStr".mutableCopy
创建了新的字符串对象,并将新对象的地址返回,最后又赋值给了mStr
;可我们知道mStr
指针是在栈上的,它随时可能被释放,直接修改就有可能造成野指针错误,这刚好对应了先前自动变量不可修改的问题;
但通过appendString
为什么又可以修改字符串呢?这主要因为mStr
通过指针传递被Block捕获后,Block只是借助其内部的指针(和mStr
同名,且指向同一个地址),找到了可变字符串的位置,向这块内存追加新的内容,但是并未改变mStr
的内存地址;
重要总结:Block修改外部变量的限制,其实是指Block不允许修改栈中指针的内容;
三、理解__block存储域类说明符的原理
通过以上的分析,我们可以将Block理解为"可以带有自动变量值的匿名函数",但由于存储域的关系,Block并不能直接修改捕获的自动变量。为了解决这个问题,总结起来有两种方案:
1.使用存储域在静态区的变量(如全局变量、静态全局变量、静态局部变量);
2.使用存储域类说明符__block;
第一种方案我们已经分析过了,现在重点来理解__block存储域说明符
的用法,其实C语言中的还有许多其他存储域类说明符,如:
typedef
extern
static
auto
register
__block说明符
就类似于static、auto、register
,它们可以用于指定变量值设置到哪个存储域中。例如,auto
表示自动变量存储在栈中(默认),static
表示静态变量存储在数据区中。
下面我们来实际使用__block
,使用它来修改被Block捕获的自动变量,具体的代码如下:
//__block存储域修饰符
int main(int argc, const char * argv[]) {
__block int val = 10;
void (^myBlock)(void) = ^{ val = 20;};
val = 30;
myBlock();
NSLog(@"val: %@",val);
return 0;
}
此处代码在Block中修改自动变量却没有像之前那样报错,说明__block说明符
是有效的,为了探究其中原理,现在我们再次把上述代码转换C++代码,具体如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 20;}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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[]) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
(val.__forwarding->val) = 30;
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_a9f88e_mi_0,(val.__forwarding->val));
return 0;
}
分析代码,我们会发现__block变量
的初始化已经发生了根本的变化,此时的自动变量val
对应的是C++源码中的__Block_byref_val_0
结构体。该结构体包含了五个成员变量,具体定义如下:
struct __Block_byref_val_0 {
void *__isa; //isa指针
__Block_byref_val_0 *__forwarding; //初始化传递的是自身结构体实例的指针
int __flags; //标记flag
int __size; //大小
int val; //对应原自动变量val的值
};
我们看到__block变量val
的初始值为10,而这个值也出现在了调用__Block_byref_val_0
结构体构造方法的时候,总结__block变量
被捕获的过程如下:
1.自动变量__block int varl
被封装为__Block_byref_val_0
结构体;
2.__Block_byref_val_0
结构体包含一个与__block变量
同名的成员变量val
,对应外部自动变量的值;
3.__Block_byref_val_0
结构体包含一个__forwarding
指针,初始化传递的是自己的地址;
4.在Block初始化的过程中,调用__main_block_impl_0
结构体构造函数时,会将__block变量
的__Block_byref_val_0
结构体实例的指针作为参数;
接下来分析给__block变量
赋值的代码,转换后的源码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 20;
}
在这里,我们看到函数首先通过cself->val
拿到了对应__block变量
的结构体实例,然后又通过__Block_byref_val_0
结构体实例的成员变量__forwarding
,最终访问到了结构体成员变量val
;具体过程如下图所示:
分析当前情况,我就会发现这里有两个很关键问题:
1.为什么要使用多余的__forwarding
指针来间接访问变量?
2.当前__block
说明符的作用仅仅体现在:将__block变量
封装为__Block_byref_val_0
结构体;这并未从根本上改变自动变量的性质,自动变量究竟是如何被修改的呢?
为了理解上述问题,我们首先应该对下面的代码有一个更加清晰的了解:
void (^myBlock)(void) = ^{ val = 10;};
代码中创建后的Block直接赋值给了强指针,这其实满足了ARC环境下编辑器对Block的优化:编译器会自动将Block从栈拷贝到堆上,而Block中的用到的__block变量也会被一并拷贝,并且被堆上的Block持有。这样即使Block语法所在的作用域结束,堆上的Block和__block变量
依然继续存在,自然也就不存在自动变量创建在栈上被释放的问题了,借助图示理解如下:
另外,当__block
变量结构体实例在从栈上被拷贝到堆上时,会将成员变量的__forwarding
的值替换为复制目标堆上的__block
变量结构体实例的地址。通过这种功能,无论是在Block语法中、Block语法外使用__block
变量,还是__block
变量配置在栈上或堆上,都可以顺利访问同__block
变量。这就是__forwarding
指针存在的意义。使用图示理解如下:
重要总结:__block
修饰的自动变量被封装为结构体,作为一个对象随着Block被拷贝到了堆上,解决了自动变量容易因作用域结束而释放的问题。而__block
变量结构体中的__forwarding
则保证了无论在栈上还是堆上访问的都是同一个__block变量
;我们能够成功修改__block
变量的值,其实是修改了堆上被Block持有的__block
变量的内部成员变量val。
其他问题:
1.ARC存在编译器的自动优化,自动拷贝Block的情况还包含了很多种,这里只是其中一种情况,上篇已分析过;
2.上述代码中,__block
说明符将基本类型的数据封装为结构体类型(其中包含了isa指针),这其实就说明__block
变量已经是作为了一个对象在使用,而对象类型被Block捕获之后都会涉及一些释放的问题,所以源码也出现了许多与对象释放相关的函数如:__main_block_copy_0
、__main_block_dispose_0
等。这个问题后续会详细分析;
四、__block变量的存储域
Block的存储域通常涉及到拷贝的操作,那么对于__block变量又是如何处理的呢?使用__block变量的Block从栈上拷贝到堆上时,__block变量也会受到影响。
1.单个Block中使用__block变量
若一个Block中使用__block
变量,则当该Block从栈拷贝到堆上时,使用的所有__block
变量也全部被从栈上拷贝到堆上。使用图示理解如下:
2.多个Block使用__block变量
多个Block使用__block
变量时,任何一个Block从栈上拷贝到堆上,__block
变量就会一并从栈上拷贝到堆上并被该Block所持有。当剩下的Block从栈拷贝到堆上时,被拷贝的Block持有__block
变量,并增加__block
变量的引用计数。使用图示理解如下:
3.__block变量的释放
如果拷贝到堆上的Block被释放,那么它使用的__block
变量的引用计数会减一,如果引用计数为0就会被释放。使用图示理解如下:
重要总结:无论是对基本类型还是对象使用__block
修饰符,从转化后的源码来看,它们都会被转化为对应的结构体实例来使用,具有引用类型数据的特性。因此__block
变量随着Block被拷贝到堆上后,它们的内存管理与普通的OC对象引用计数内存管理模式完全相同。
五、理解Block对对象的捕获
仔细观察之前的源码我们就会发现,Block捕获对象类型和__block
类型的变量(在底层被封装为结构体,也属于对象)明显比基本类型要复杂多,其实这里主要是因为对象类型还要涉及到释放的问题。下面的代码演示了Block对对象的捕获的过程,具体如下:
typedef void(^AddBlock)(NSString *); //定义一种携带字符串参数的Block
int main(int argc, const char * argv[]) {
AddBlock blk = nil;
{
NSMutableArray *mArr = @[].mutableCopy;
blk = ^(NSString *string){
[mArr addObject:string];
NSLog(@"mArr count = %ld",[mArr count]);
};
}//NSMutableArray所在的作用域结束
blk(@"A");
blk(@"B");
blk(@"C");
return 0;
}
//打印结果:
mArr count = 1
mArr count = 2
mArr count = 3
分析代码:当前为ARC环境下,编译器自动对访问了自动变量的mArr
的blk
进行了拷贝;所以mArr
离开其所在的作用域结束时并没有被释放。虽然mArr
指针已经不能使用,但是blk
依然保留有对mArr
的引用可以找到这块内存。所以代码也是运行正常的;
现在查看编译器转换后的源码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *mArr;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_mArr, int flags=0) : mArr(_mArr) {
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->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mArr, 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};
由于代码量较大,这里只提供了与捕获基本类型不同的部分;我们发现,当Block捕获对象类型的变量时,此处的__main_block_desc_0
结构体中多了copy
与dispose
两个成员变量,而且它们的初始化分别使用了__main_block_copy_0
和__main_block_dispose_0
的函数指针;
这里主要的原因是,在Objective-C中,C语言结构体不能含有__strong、__weak
修饰符的变量,因为编译器不知道应该如何进行C语言结构的初始化和废弃操作,不能很好地管理内存。但是OC的运行时库能够准确把握Block从栈复制到堆以及堆上Block被废弃的时机,所以这里才会增加与内存管理相关的变量和函数。
1.__main_block_copy_0函数
结构体__main_block_desc_0
中的copy
成员变量对应了__main_block_copy_0
函数。
当Block从栈上拷贝到堆上时,__main_block_copy_0
函数会被调用,然后再调用其内部的_Block_object_assign
函数。_Block_object_assign
函数就相当于retain操作,会自动根据__main_block_impl_0
结构体内部的mArr
是什么类型的指针,对mArr
对象产生强引用或者弱引用。如果mArr
指针是__strong
类型,则为强引用,引用计数+1,如果mArr
指针是__weak
类型,则为弱引用,引用计数不变。
2.__main_block_dispose_0函数
结构体__main_block_desc_0
中的dispose
成员变量对应了__main_block_dispose_0
函数。
当Block被废弃时,__main_block_dispose_0
函数会被调用,__main_block_dispose_0
函数就相当于release操作,将mArr
对象的引用计数减1,如果此时引用计数为0,那么遵循引用计数的规则mArr
也就被释放了。
3.Block捕获对象与__block变量的区别
其实Block捕获对象与__block
变量后,对于它们的内存管理的方式相同,也都是使用copy函数持有和disposde函数释放;两者体现在源码上的不同,我们可以观察下面的函数:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
_Block_object_assign
函数中的最后一个参数用于区分Block捕获的是对象还是__block
变量。
对象变量 | __block变量 |
---|---|
BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BYREF |
六、Block的循环引用问题
Block在从栈拷贝到堆上时,如果其中捕获了强类型的对象,该对象就会被Block所持有。这样很容易就会引起循环引用,我们来看下面的代码:
typedef void(^MyBlock)(void);
@interface MyObject : NSObject
@property(nonatomic,copy) MyBlock block;
@end
@implementation MyObject
- (instancetype)init {
self = [super init];
return self;
}
- (void)dealloc {
NSLog(@"MyObject dealloc!");
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
MyObject *myObject = [[MyObject alloc] init];
myObject.block = ^{
//Capturing 'myObject' strongly in this block is likely to lead to a retain cycle
NSLog(@"捕获对象:%@", myObject );
};
}
NSLog(@"myObject的作用域结束了");
return 0;
}
不仅编译器给出了内存泄漏的警告,而且测试结果也证实了MyObject的dealloc
实例方法并没有执行,这里发生了循环引用。原因就在与myObject
的block
在被自动拷贝到堆上的过程中持有了myObject
,而myObject
本身就持有了block
,所以两者相互持有就产生了问题。
现在就来总结类似情况下的Block循环引用的处理方法,可分为ARC和MRC两种情况:
1.解决ARC环境下的循环引用问题
方法1:使用弱引用修饰符__weak、和__unsafe_unretained修饰符;
使用__weak解决上述问题,需要改进的代码如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
MyObject *myObject = [[MyObject alloc] init];
__weak typeof(myObject) weakObject = myObject;
myObject.block = ^{
NSLog(@"捕获对象:%@", weakObject );
};
}
NSLog(@"myObject的作用域结束了");
return 0;
}
上述代码使用弱引用修饰符__weak ,在block内部对 myObject
设置为弱引用,弱引用不会导致Block捕获对象的引用计数增加(这在上述分析中已经讲过)。
注意__weak
和__unsafe_unretained
的区别:
__weak:iOS4之后才提供使用,而且比__unsafe_unretained
更加安全,因为当它指向的对象销毁时,会自动将指针置为nil;推荐使用。
__unsafe_unretained:在__weak
出现以前常用修饰符,其指向的对象销毁时,指针存储的地址值不变,所以没有__weak
安全。
方法2:使用__block说明符
回忆__block
修饰基本类型的C++源码,我们可以知道__block
修饰对象时其实也会封装一个结构体类型,而这个结构体中会持有自动变量对象,这样就会造成下图的情况:
使用__block
解决上述问题,需要改进的代码如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
MyObject *myObject = [[MyObject alloc] init];
__block MyObject *tempObject = myObject;
myObject.block = ^{
NSLog(@"捕获对象:%@", tempObject );
tempObject = nil; //关键代码1
};
myObject.block(); //关键代码2:执行持有的block;
}
NSLog(@"myObject的作用域结束了");
return 0;
}
上述代码有两句关键,已经通过注释标注;在block中通过tempObject = nil
这句代码,__block
变量tempObject
对于MyObject类对象的强引用失效了,而这句代码生效的前提又是block被调用了(关键代码2);这种方式避免了循环引用的产生的过程如下图:
特别注意:如果关键代码2没有被调用,同样会造成循环引用。
使用__block变量相比弱引用修饰符的优缺点:
优点:
1.通过执行block的方式,可动态决定__block
变量可以控制对象的持有时间;
2.在不能使用__weak
修饰符的环境下,避免使用__unsafe_unretained
(因为要考虑野指针问题);
缺点:
为了避免循环引用,必须执行Block;
2.解决MRC环境下的循环引用问题
方法1:使用弱引用修饰符__unsafe_unretained修饰符;
在MRC环境下不支持使用__weak
,所以只能使用__unsafe_unretained
;使用原理同ARC环境下相同,这里不再赘述。
方法2:使用__block说明符
MRC环境下,__block
说明符被用来避免循环引用。这是因为当Block从栈拷贝到堆时,若Block使用的变量是附有__block
说明符的id类型或者对象类型的自动变量,不会被retain,否则就会被retain。这一点和ARC环境是不同的。现在我们在MRC环境下改进代码,具体如下:
int main(int argc, char * argv[]) {
MyObject *myObject = [[MyObject alloc] init];
__unsafe_unretained MyObject *tempObject = myObject;
myObject.block = ^{
NSLog(@"捕获对象:%@", tempObject );
};
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[myObject autorelease];
[pool drain]; //等同于[myObject release];
return 0;
}
//打印结果:
//MyObject dealloc!
上述操作将代码改为了MRC下的自动释放池,相比之前在ARC中使用__block
,这里没有在Block内部置nil的操作,也没有调用block,但同样解决了循环引用的问题;
重要总结:__block
说明符在ARC与MRC环境下的用途有很大区别,因此在编写代码时我们必须区分好这两种环境。
网友评论