说起 Block,我想大家再熟悉不过了。每天的开发过程中都在与他打着交道,简洁高效的使用给我们的工作带来了便利。
正是因为Block对于我们而言不可或缺,我更觉得应该抽出时间好好认识一下Block,了解一些他的优缺点、过往经历,让我们对它的理解更深一层,进而开发出更加高效的程序。
对Block的认识
Block与C函数的区别和联系
Block是C语言的扩充,常被称为带有局部变量的匿名函数;
看一下正规的C语言函数定义:
int test(int count);
int result = test(10);
以上声明了函数名为test
函数;
使用函数指针调用函数:
int (*testFuncPtr)(int) = &test;
int result = (* testFuncPtr)(10);
对比Block,Block就可以定义为不带名称的函数,说的直白一点,就是匿名函数
,即不用显式的定义函数名称,但是匿名函数
都有固定的一个表达式。
比如:
^(int count){
printf("%d", count);
}
等价于:
void test(int count){
}
Block
语法定义格式如下:
^ 返回值类型 参数列表 表达式
省略之后:
^ 表达式
通过上面的例子可以看出两点不同:
- 没有函数名称;
- 带有“^”符号;
Block的基本使用
可以通过定义Block类型的变量对Block进行调用,与函数指针有相似之处,具体如下:
int (^block)(int) = ^(int count){
return count + 1;
}
block(5);
Block类型变量用途:局部变量、参数、静态变量、全局变量以及静态全局变量等等。
Block 可以作为参数进行传递,也可以作为返回值进行传递,具体如下:
- (void)testFuncWithBlock:(void (^)(int count))block{
if (block) {
block(5);
}
}
// 将Block作为返回值,代码摘自·Masonry·
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
具体以Masonry
使用为例讲解一下,Block调用:
// Masonry 在常用布局时用到方法如下:
make.top.equalTo(self.titleLabel.mas_bottom).offset(15);
// 有些同学可能对上述的链式调用不是很清楚,其实上述链式调用就是使用Block实现的,分解如下:
MASConstraint *constraint = make.top;
MASConstraint *(^equalBlk)(id) = constraint.equalTo;
constraint = equalBlk(self.titleLabel.mas_bottom);
MASConstraint *(^offsetBlk)(CGFloat) = constraint.offset;
constraint = offsetBlk(15);
可使用关键字typedef
,进行Block类型变量的定义
typedef int(^Blk)(int);
// 调用
Blk blk = ^int (int count){
return count ++;
};
int result = blk(5);
Block 作为属性变量的用法:
@property (nonatomic, copy) void (^successCompletion)(int);
@property (nonatomic, copy) Blk failCompletion;
通过上面一小节了解到:
- 函数与Block区别与联系;
- Block变量和Block表达式的联系
Block的实质
在前面小节中,介绍了Block
的基本用法。那么问题来了,Block
究竟是什么呢?现在公认的说法是带有自动变量的匿名函数
,动手一探究竟。
使用Clang -rewrite-objc
命令对下面代码进行编译:
void testBlock(){
int (^blk)(int) = ^(int count){
return count ++;
};
blk(5);
}
执行完命令会在同级目录下生成XX.cpp
文件,这个文件就是编译好的文件。
由于编译后文件包含很多C函数声明,在此就不一一解释了,主要关注上述Block
方法实现:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;�
Desc = desc;
}
};
static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
return count ++;
}
函数具体的实现如下:
void testBlock(){
int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
}
虽然编译后Block
方法很长,但是并不复杂,Block
调用就像普通的C函数
的使用。
- 首先,看一下源代码中
Block
方法体实现:
int (^blk)(int) = ^(int count){
return count ++;
};
static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
return count ++;
}
可以发现两者在语法格式上非常相近,实际上Block
所使用的匿名函数就是被作为简单的C函数
处理的。
在代码中:
__testBlock_block_impl_0 *__cself
其中,参数__cself
相当于C++中指向实例自身变量的this
或者Objective-C中的self
。参数__cself
在这里是__testBlock_block_impl_0
结构体的指针。
- 然后,看一下
__testBlock_block_impl_0
结构体的组成:
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
};
去掉代码中加入的构造函数,感觉结构上清晰多了。第一个成员变量是impl
,__block_impl
结构体的声明如下所示:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
其中,有关对象isa指针的理解可以参考之前写过的文章Objective-C Runtime:深入理解类与对象
第二个成员变量Desc
,主要保存block所在内存的区域以及Block的大小。
- 关于
__testBlock_block_impl_0
结构体构造函数如下:
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;�
Desc = desc;
}
isa指针指向_NSConcreteStackBlock
, __testBlock_block_impl_0
结构体相当于objc_object
结构体的Objective-C类对象的结构体。
isa = &_NSConcreteStackBlock;
上述代码表明_NSConcreteStackBlock
相当于objc_class
结构体的实例。从而得知,Block就是Objective-C的对象了。
具体关于Objective-C类与对象的知识可以参考另一篇文章深入理解类与对象。
对于上述构造函数调用部分如下:
int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
看着有些繁琐,对其去掉相关转换的部分:
struct __testBlock_block_impl_0 temp = __testBlock_block_impl_0(__testBlock_block_func_0, &__testBlock_block_desc_0_DATA);
struct __testBlock_block_impl_0 *blk = & temp;
(*blk->impl.FuncPtr)(blk, 5);
上述代码中,将__testBlock_block_impl_0
结构体实例的指针赋值给变量blk
,从而知道源代码中的Block表达式就是__testBlock_block_impl_0
结构体类型的变量,同时也是该结构体在栈上生成的实例。
从函数指针调用得知,由Block语法转换的__testBlock_block_func_0
函数指针被赋值到__testBlock_block_impl_0
的成员变量FuncPtr
,同时说明了__testBlock_block_func_0
中的__cself
指向Block值。
截获局部变量
在开发过程中,我们经常遇到在Block中截获局部变量或者改变局部变量,如下所示:
- (void)testBlock{
int value = 1;
void (^block) (void) = ^{
NSLog(@"value = %d", value);
};
block();
}
看到这里,可能就有疑问了,Block内部是如何获取局部变量的,带着疑问重新Clang了一下源文件,似乎找到了答案,摘取了关键代码如下所示:
static void _I_TestBlock_testBlock(TestBlock * self, SEL _cmd) {
int value = 1;
void (*block) (void) = ((void (*)())&__TestBlock__testBlock_block_impl_0((void *)__TestBlock__testBlock_block_func_0, &__TestBlock__testBlock_block_desc_0_DATA, value));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
//Block内部实现:
static void __TestBlock__testBlock_block_func_0(struct __TestBlock__testBlock_block_impl_0 *__cself) {
int value = __cself->value; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pf_nyb_8kl12yq6v2sxzwdr5v8w0000gn_T_TestBlock_516a43_mi_0, value);
}
// Block结构体
struct __TestBlock__testBlock_block_impl_0 {
struct __block_impl impl;
struct __TestBlock__testBlock_block_desc_0* Desc;
// 将局部变量追加成成员变量
int value;
__TestBlock__testBlock_block_impl_0(void *fp, struct __TestBlock__testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
通过上述Block语法表达式中使用的局部变量作为成员变量被加到了__TestBlock__testBlock_block_impl_0
结构体中,同时,加到结构体中的成员变量的类型与局部变量的类型保持一致。当然了,如果Block表达式中没有使用局部变量,则不会加入到Block的结构体中。
通过初始化Block实例的构造方法和调用也可以看到局部变量value
作为参数传递,具体如下所示:
//Block 初始化方法
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value)
// 初始化方法的调用:
void (*block) (void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, value));
到这里,大致明白了Block如何截获局部变量value
的,小结如下:
- 1、系统编译时,根据Block是否需要截获外部变量来判断是都将局部变量作为成员变量保存到Block的结构体
__testBlock_block_impl_0
中; - 2、若需要截获局部变量,需要在初始化结构体函数中添加与局部变量相同类型的参数;
- 3、局部变量就通过初始化构造函数传递到Block结构体
__testBlock_block_impl_0
,同时赋值给Block中的成员变量; - 4、在执行Block时,局部变量
value
会初始化__testBlock_block_impl_0
结构体实例, 如下所示;
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __testBlock_block_func_0;
Desc = &__testBlock_block_desc_0_DATA;
value = 1;
总的来说,截获局部变量就是将要使用的局部变量保存到Block结构体实例中。
Block中使用C语言数组
- 只使用C语言的字符串数组,也没有向捕获的局部变量赋值,然而下面的代码再编译时就会产生错误,如下所示:
int testBlock1(){
const char country[] = "China";
void (^block) (void) = ^{
printf("text=%c", country[1]);
};
block();
return 0;
}
编译的时候会出现错误:
看到这,你会感到疑惑,上面刚讲到Block是如何截获局部变量的,为什么到这里就失效了呢?
这是因为,Block中,没有实现对C语言数组的截获。通过上述截获局部变量的理论,C语言数组会作为成员变量保存到Block实例的结构体中,在初始化Block时,由成员变量赋值给局部变量,猜测代码如下:
void func(char a[]){
char b[] = a;
}
从而看出端倪,将C语言数组类型变量赋值给C语言数据类型变量,这个在C语言下是无法编译,C语言不允许这样编译的。
如何解决呢?
目前,可以改成C语言数组的形式就可以解决上述问题了。
int testBlock1(){
const char *country = "China";
void (^block) (void) = ^{
printf("text=%c", country[1]);
};
block();
return 0;
}
__block关键字的作用
前面的小节中,我们仅仅讲解了在Block中截获局部变量,那么,在Block中修改截获的局部变量又会带来什么样的问题呢?
通过上面的代码可以看出,虽然在Block中不能对局部变量进行修改,但是在全局变量、静态全局变量、静态变量都是可以在Block中进行修改的,具体如下:
int globalValue = 10;
static int staticGlobalValue = 5;
- (void)testBlockFunc {
static int value = 5;
void (^block)(void) = ^{
globalValue = 11;
staticGlobalValue = 12;
value = 3;
printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, value);
};
block();
}
打印的结果如下:
2018-12-02 21:56:47.323727+0800 TestBlock[38880:1182883]
globalValue = 11
staticGlobalValue = 12
value = 3
再次使用clang编译了一下,发现静态全局变量和全局变量转换前后没有任何变换,大家可能疑问静态局部变量是如何转换的呢?
__TestBlockMemory__testBlockFunc_block_impl_0 *__cself) {
int *value = __cself->value; // bound by copy
globalValue = 11;
staticGlobalValue = 12;
(*value) = 3;
printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, (*value));
}
与截获局部变量的转换很相似,唯一的区别是Int类型的变量转换成了指针,从而正确的改变了原有的值。
除了上述的几种方式可以在Block中修改变量值外,OC专门提供了一个说明符——__block。
int testBlock(){
__block int number = 1;
void (^block) (void) = ^{
number = 2;
};
block();
return 0;
}
将上述代码进行Clang编译,还原成以下代码:
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;
int __flags;
int __size;
int number;
};
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__Block_byref_number_0 *number; // by ref
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
__Block_byref_number_0 *number = __cself->number; // bound by ref
(number->__forwarding->number) = 2;
}
惊奇的发现,局部变量number变成了结构体__Block_byref_number_0,Block的__testBlock_block_impl_0的结构体实例持有__block变量的__Block_byref_number_0结构体实例的指针。__testBlock_block_impl_0实例的成员变量__forwarding指向实例自身,并通过__forwarding变量访问成员变量** number**。
__Block_byref_number_0 *number = __cself->number; // bound by ref
(number->__forwarding->number) = 2;
值得注意一点是,__Block_byref_number_0结构体并没有在__testBlock_block_impl_0结构体中,这样主要是为了在多个Block中共用该__block变量。
Block的储存
Block分为三类:** _NSConcreteStackBlock**
种类 | 存储域 | Block变量类型 | 复制后的存储域 |
---|---|---|---|
_NSConcreteStackBlock | 栈 | 局部/自动变量 | 由栈复制到堆上 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) | 全局变量 | 保持在数据区 |
_NSConcreteMallocBlock | 堆 | 局部变量 | 增加引用计数 |
定义在全局的Block在变量作用域之外通过指针访问使用,定义在栈上的__block局部变量和Block在超出作用域时将被废弃。那Block超出作用域是如何存在的呢?
这里就用到Block的Copy操作,即将分配在栈上的Block 和__block变量复制到堆上,从而延长了Block和__block变量的生命周期。
那么,Block被复制到堆上的操作是何时进行的呢?
首先,比较明确的一点是:将Block作为返回值返回时,编译器将会自动将Block由栈上复制到堆上,其他情况下,需要手动执行Copy操作;在ARC有效的情况下,Block是否被复制到堆上,编译器会进行相关的判断;
举个栗子:
typedef int (^BlockCase)(int);
BlockCase getBlockCallBack(int value) {
return ^(int count) {
return count + value;
};
}
在ARC有效的情况下,作为返回值的Block会被复制到堆上。当然了,系统中存在的一些API是无需手动复制的:Cocoa框架方法中带有usingBlock方法、GCD相关的API等等。
__block变量的存储域
当在Block中使用__block变量时,当在Block从栈复制到堆上同时,被使用过的__block变量也会从栈复制到堆上。如下图所示:
__block变量所分配的存储域 | Block从栈复制到堆上后的影响 |
---|---|
栈 | 由栈复制到堆上并被Block持有 |
堆 | 被Block持有 |
__fawarding指针存在的意义
前边介绍了Block和__block变量从栈复制到堆上的情景,但都与__fawarding指针脱不开关系。
- 由栈复制到堆之前,__fawarding指针指向自身;
- 复制之后,栈上的__fawarding指针指向复制到堆上的__block变量,堆上的__fawarding指针指向自身。
从而,解释了无论__block变量配置在栈上还是配置在堆上时都能正确的访问到__block变量;
Block截获对象
通过了解上节的内容,很好理解以下内容:
- __block修饰的变量从栈复制到堆上,赋值给该__block变量的对象也被从栈复制到堆上,当__block变量从堆上释放时,该对象才能得到释放;
- 当使用__weak修饰的__block变量在赋值的时候,由于赋值对象的作用域问题而释放,从而导致__block变量不能强持有该对象。
Block循环引用
形成原因:
- 对象与Block相互持有;
- 形成强持有环;
解决方式: - 设置weak弱持有关系;
- 手动置为nil,梳理好持有关系,破坏环状结构。
结束
关于Block就大致说到这吧,从Block的使用到Block的原理剖析,希望能让我们更客观的认识Block,写出更高质量的代码。
好久没有更新文章了,想必老铁们也都等着急了吧。主要是从2018年3月底更新后最后一篇文章,就一直在忙,关于那段时间的事情会在另一篇文章中详述。
谢谢大家的支持!
扫一扫下面的二维码,欢迎关注我的个人微信公众号:猿视角(ID:iOSDevSkills),可在微信公众号进行留言,更多精彩技术文章,期待您的加入!一起讨论,一起成长!一起攻城狮!