主要内容:
- 分析
Block
捕获外部变量的过程 - 理解
Block
修改外部变量的限制 - 分析
__block
存储域类说明符的原理 - 理解
__block
变量的存储域 - 探究
Block
对对象的捕获过程 -
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
里被修改,而且直接修改自动变量
就会报错;
此时,考虑以下两个问题:
- 为什么在
Block
里不允许更改自动变量? -
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)
3.静态局部变量也在静态区,为什么不可以像全局变量一样直接修改?
其实,关键原因还是"局部"
两个字,我们看到C++
代码中的函数__main_block_func_0
被设置在了包含Block
语法的main
函数之外,而静态局部变量就是在main
函数中定义的;
所以,__main_block_func_0
和静态局部变量
的作用域是不同的,当然不能像全局变量一样随时访问它。因此,还是采用了捕获和指针传递的方式来修改静态局部变量
。
4.为什么自动变量不能像静态变量一样指针传递呢?
这主要还是因为自动变量
和静态变量
的存储域的不同。
自动变量存在栈上,其被销毁的时间不定,这很有可能导致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
理解为"可以带有自动变量值的匿名函数
",但由于存储域的关系,Block
并不能直接修改捕获的自动变量。为了解决这个问题,总结起来有两种方案:
- 使用存储域在静态区的变量(如
全局变量
、静态全局变量
、静态局部变量
); - 使用存储域类说明符
__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变量
被捕获的过程如下:
- 自动变量
__block int varl
被封装为__Block_byref_val_0
结构体,保存原始变量的指针和值; -
__Block_byref_val_0
结构体包含一个与__block变量
同名的成员变量val
,对应外部自动变量的值; -
__Block_byref_val_0
结构体包含一个__forwarding
指针,初始化传递的是自己的地址; - 在
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
;
具体的过程,如下图所示:
分析当前情况,我就会发现这里有两个很关键问题:
- 为什么要使用多余的
__forwarding
指针来间接访问变量? - 当前
__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;
其他问题:
-
ARC
存在编译器的自动优化,自动拷贝Block
的情况还包含了很多种,这里只是其中一种情况,上篇已分析过; - 上述代码中,
__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
变量也全部被从栈上拷贝到堆上。使用图示理解如下:
<img src="Pictures/Block原理探究/Block原理-在一个Block中使用__block变量.png" width="500" hegiht="313" align=center/>
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变量相比弱引用修饰符的优缺点:
优点:
- 通过执行
block
的方式,可动态决定__block
变量可以控制对象的持有时间; - 在不能使用
__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
环境下的用途有很大区别,因此在编写代码时我们必须区分好这两种环境;