《Objective-C高级编程:iOS与OS X多线程和内存管理》是iOS开发中一本经典书籍,书中有关ARC、Block、GCD的梳理是iOS开发进阶路上必不可少的知识储备。笔者读完此书后为了加强理解,特以笔记记之。本文为中篇,主要谈论Objective-C中的Block。
鉴于本书翻译自日文原版且翻译偏向书面,笔者希望采用通俗的语言记录,文章结构略有调整。
本文首发于Rachal's blog。
Block概要
定义:Block是带有自动变量(局部变量)的匿名函数。
匿名函数就是不带名称的函数。
C语言的函数中可能使用的变量:
- 自动变量(局部变量)
- 函数的参数
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
前两种在超出作用域会销毁,后三种可以在函数的多次调用之间传递。
在计算机科学中,“带有自动变量值的匿名函数”这一概念称为闭包,Block就是Objective-C对闭包的实现。
Block模式
Block语法
Block的完整语法:^
返回值类型
参数列表
表达式
^ int (int count) {return count + 1;}
完整形式的Block语法与一般C语言函数定义相比,仅有两点不同:
- 没有函数名
- 带有
^
其中返回值类型和参数列表可省略,省略后为:^
表达式
^ {printf("Blocks\n");}
Block变量类型
在Block语法中下,可将Block语法赋值给声明为Block类型的变量。
int (^blk)(int);// 声明Block类型
int (^blk1)(int) = ^(int count) {return count + 1};// 变量blk1
int (^blk2)(int);// 变量blk2
blk2 = blk1;// Block类型变量赋值
通过typedef
可声明Block类型变量,函数定义就变得更容易理解。
typedef int (^blk)(int);
blk blk1 = ^(int count) {return count + 1;};
blk blk2 = blk1; // blk类型变量赋值
截获自动变量
- Block可以截获自动变量的值。
NSInteger aa = 10;
void (^blk)(void) = ^{
NSLog(@"%ld",aa);
};
aa = 2;
blk();
// 输出结果为10
- Block中使用时向截获的自动变量进行赋值会出错。
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
id obj = [[NSObject alloc] init];
/* 使用array变量调方法,没问题 */
[array addObject:obj];
};
blk();
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
/* 想array变量赋值,出错 */
array = [[NSMutableArray alloc] init];
};
blk();
__block说明符
若想在Block语法的表达式中将值赋给在Block语法外声音的自动变量,需要在该自动变量上附加__block
说明符。
__block int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n",val);// 输出结果为val = 1;
__block id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
/* 向array变量赋值,不会出错 */
array = [[NSMutableArray alloc] init];
};
blk();
Block实现
Block的实质
将Objective-C代码转化成可读源码(C++)的方法:
// 终端cd 源代码文件夹
clang -rewrite-objc 源代码文件名
书中将一段Block代码转化为可读的C++源码,简化源码,关注其中的结构体:
struct __main_block_imp_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
}
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
Block的结构体中含有isa
指针,足以证明Block即为Objective-C的对象。
isa = &_NSConcreteStackBlock;
isa
被初始化,说明在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSCocreteStackBlock
中。
三种Block
前面提到了_NSConcreteStackBlock
类型的Block,与之对应的还有_NSConcreteGlobalBlock
、_NSConcreteMallocBlock
。
名称 | Block的类 | 对象存储域 | 复制效果 |
---|---|---|---|
栈Block | _NSConcreteStackBlock | 栈 | 从栈复制到堆 |
堆Block | _NSConcreteMallocBlock | 堆 | 引用计数增加 |
全局Block | _NSConcreteGlobalBlock | 程序的数据区(.data区) | 什么也不做 |
全局Block产生途径:
- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用应截获的自动变量时
除此之外的Block语法生成的Block为栈Block,将栈上的Block复制到堆上,Block就会变成堆Block。
Blobk如何截获自动变量值
通过clang源码转换得知:
Block语法表达式中使用的自动变量被作为成员变量追加到Block的结构体中。Block语法表达式中没有使用的自动变量不会被追加。Block的自动变量截获只针对Block中使用的自动变量。
所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的的自动变量值被保存到Block的结构体实例(即Block自身)中。
__block变量
__block
说明符能用来指定Block中想变更值的自动变量。
准确的表述方式为“__block存储域类说明符”,C语言中有以下存储域类说明符:
- typedef
- extern
- static
- auto
- register
它们用于指定将变量值设置在哪个存储域中。例如:auto
表示作为自动变量存储在栈中,static
表示作为静态变量存储在数据区。
自动变量加上__block
,源代码会急剧增加,变量的转换如下:
__block int val = 10;
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
自动变量转换后竟然变成了结构体实例,即栈上生成的__Block_byref_val_0
结构体实例。
结构体声明如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
__Block_byref_val_0
结构体实例的成员变量__forwarding
持有指向该实例自身的指针,也就是原自动变量。
另外,__block
变量的__Block_byref_val_0
结构体并不在Block的结构体中,这样做是为了在多个Block中使用__block
变量。
Block从栈复制到堆时,
__block
变量也会一并从栈复制到堆并被该Block所持有。在多个Block中使用__block
变量时,被复制的Block持有__block
变量,并增加__block
变量的引用计数。如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。
栈上的__block
变量的结构体实例在复制到堆上时,会将成员变量__forwarding
的值替换为复制目标堆上的__block
变量的结构体的地址。
通过该功能,无论是在Block语法中、Block语法外使用__block
变量,还是__block
变量配置在栈上或堆上,都可以顺利访问同一个__block
变量。
Block截获的对象和__block变量
- (void)blockTest {
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"array count is %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
以上blockTest
方法在ARC无效和有效情况下执行结果:
- ARC无效时,程序会强制结束。因为对象
array
超出作用域后不存在。 - ARC有效时,输出如下:
array count is 1
array count is 2
array count is 3
ARC有效时,大部分情况下编译器会适当的进行判断,自动生成将Block从栈上复制到堆上的代码。被栈上Block截获的对象可以超出作用域而存在。
通过编译器转换后的源码查看Block结构体,发现Block结构体中有__strong
修饰的成员变量array
。
struct __main_block_impl_0 {
struct __blobk_impl impl;
struct __main_block_desc_0* Desc;
id __strong array;
}
C语言结构体中不能含有__strong
修饰的变量,因为编译器不能很好地对其内存管理。但是Objective-C运行时库能够准确把握从栈复制到堆上的Block被废弃的时机,恰当地对变量进行初始化和废弃。
在此对比截获对象和使用__block
变量时持有(copy
)和废弃(dispose
)函数源码:
- 截获对象:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
- 使用
__block
变量:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
通过对比发现:
- 截获对象和
__block
变量使用_Block_object_assign
函数 - 废弃对象和
__block
变量使用_Block_object_dispose
函数 - 通过Block
_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
参数区分copy
和dispose
函数的对象类型是对象还是__block
变量。
由此可知:Block中使用的赋值给__strong
修饰(ARC下常省略)的自动变量的对象和复制到堆上的__block
变量,由于被堆上的Block所持有,因而可超出其变量作用域而存在。
栈上的Block复制到堆上的时机:
- 调用Block的
copy
方法时 - Block作为函数返回值返回时
- 将Block赋值给
__strong
修饰的id类型或Block类型变量时 - 在方法名中含有
usingBlock
的Cocoa框架方法或GCD的API中传递Block时
栈上的Block为何需要复制到堆上
将栈上的Block复制到堆上,这样即使Block记述的变量作用域结束,堆上的Block还可以继续存在。
- (void)blockTest {
id obj = [self getBlockArray];
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
}
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);},nil];
}
执行以上blockTest
方法会出错,原因在于变量作用域。如下修改后没问题:
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d",val);} copy],[^{NSLog(@"blk1:%d",val);} copy],nil];
}
注意:将Block从栈上复制到堆上是相当消耗CPU的。
Block循环引用
Block为何会引起循环引用
Block中使用__strong
修饰的对象类型自动变量,当Block从栈复制到堆时,该对象为Block所持有,这样容易引起循环引用。
typedef void(^blk_t) (void);
@interface MyObject : NSObject {
blk_t _blk;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
_blk = ^{
NSLog(@"self = %@", self);//循环引用警告
};
return self;
}
@end
int main() {
id o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
MyObject类对象实例self持有Block,Block语法中使用了self,Block从栈复制到堆并持有所使用的self。self和Block相互持有,引起循环引用。
__weak解决循环引用
为避免循环引用,可声明__weak
修饰的变量,并将self赋值使用。
- (id)init {
self = [super init];
id __weak tmp = self;
_blk = ^{
NSLog(@"self = %@", tmp);
};
return self;
}
__weak
打破了Block对self的强引用,解决了循环引用问题。
另外,Block内使用MyObject类的实例变量也会引起循环引用,因为会造成Block对self的间接持有。同样使用__weak
可解决循环引用问题。
typedef void(^blk_t) (void);
@interface MyObject : NSObject {
blk_t _blk;
id _obj;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
id __weak tmp = _obj;
_blk = ^{
NSLog(@"_obj = %@", _obj);
};
return self;
}
@end
__block解决循环引用
使用__block
变量也可以避免循环引用。
typedef void(^blk_t) (void);
@interface MyObject : NSObject {
blk_t _blk;
}
- (void)execBlock;
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
_blk = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
blk();
}
@end
int main() {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
使用__block
变量来避免循环引用注意以下两点:
-
__block
变量在使用完时要被赋值nil
- 必须执行Block
PS:笔者认为这种方式其实是在模拟__weak
功能。因为__weak
修饰的变量在超出作用域销毁时会自动被赋值nil
,而执行Block刚好就是促使变量被赋值nil
这一操作执行。
注意:ARC有效和无效时,__block
说明符的区别很大。ARC无效时,__block
说明符用来避免Block中的循环引用,因为有__block
说明符的变量不会被retain
。
以上为Block篇的学习内容。