为了解决多线程问题,苹果公司以全新的方式设计了多线程。核心就是“块”(block)与“大中枢派发”(Grand Central Dispatch, GCD)。
“块”是一种可在C、C++及Objective-C代码中使用的“词法闭包”,借由此机制,开发者可将代码向对象一样传递,令其在不同环境下运行。
GCD是一种与块有关的技术,它提供了对线程的抽象,这种抽象则基于“派发队列”。开发者可将块排入队列中,由GCD负责所有调度事宜。GCD会根据系统资源情况,创建、复用、摧毁后台线程,以便处理每个队列。
理解“块”这一概念
块可以实现闭包。这项语言特性是作为“扩展”而加入GCD编译器中的。
块的基础知识
块类似于直接定义在另一个函数里的函数,和定义它的函数共享一个范围内的东西。
定义简单块的例子:
^{
//Block implementation here
}
块其实是一个值,有其相关类型。也可以把块赋给变量,然后像使用其他变量那样使用它。
例,没有参数,不返回值的块:
void (^someBlock) () = ^{
//Block implementation here
};
这段代码定义了一个名为someBlock的变量。块类型的语法结构如下:
return_type (^block_name)(parameters)
下面这种写法所定义的块,返回int值,并且接受两个int做参数:
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b;
};
使用:
int add = addBlock(2, 3);
NSLog(@"%d", add); // 5
块的强大之处是:在声明它的范围里,所有的变量都可以为其所捕获。比如,下面这段代码所定义的块,就使用了块以外的变量:
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b + additional;
};
int add = addBlock(2, 5);
NSLog(@"%d", add); // 12
默认情况下,为块所捕获的变量,是不可以在块里修改的。如果想在块里修改变量,可以在声明变量时加上__block修饰符。
例如本例,在块内修改additional:
__block int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
additional++;
return a + b + additional;
};
int add = addBlock(2, 5);
NSLog(@"%d %d", add, additional); // 13, 6
内联块
不把块赋给局部变量,而是内联到函数调用里。这样可以把逻辑调用都放在一处。
例,用块来枚举数组中的元素,判断其中有多少个小于2的数:
NSArray* array = @[@0, @1, @2, @3, @4, @5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber* number = (NSNumber*)obj;
if ([number compare:@2] == NSOrderedAscending) {
count++;
}
}];
块与对象类型
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候也会将其一并释放。
实际上,块本身可视为对象,很多其他Objectivec-C可响应的选择子中,有很多,块也可以响应。而最重要之处在于块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量。
如果将块定义在Objective-C的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获了因为实例变量是与self所指代的实例关联在一起的。
例如,下面这个块声明在EOCClass类的方法中:
@implementation EOCClass
- (void) anInstanceMethod {
void (^someBlock)() = ^{
_value = @"Something";
NSLog(@"%@", _value);
};
}
@end
如果某个EOCClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。直接访问实例变量和通过self来访问是等效的:
self->_value = @"Something";
之所以要捕获self变量,原因正在于此。我们经常通过属性访问实例变量,在这种情况下,就要指明self了:
self.value = @"Something";
块的内部结构
块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。
invoke
在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。
invoke函数需要把块对象作为参数传进来,以便于在执行块时,把捕获到的变量从内存中读出来。
descriptor
descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块时运行,其中会执行一些操作。
块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。
全局块、栈块及堆块
栈块
定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围生效。例如,下面这段代码就很危险:
void (^block)();
if (...) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
block();
定义在if及else语句中的两个块都分配在栈内存中,等离开了相应的范围,编译器很可能吧分配给栈的内存覆写掉。于是,这两个块只能保证在对应的if或else语句内有效。
堆块
未解决此问题,可给块对象发生copy消息,以拷贝。这样的话,就可以吧块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且一旦复制到堆上,块就成了带了引用计数的对象了。后续的复制操作都只是递增块对象的引用计数,
例:
void (^block)();
if (...) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();
现在代码就安全了。如果手动管理引用计数,那么在用完块之后还需要将其释放。
全局块
这种块不会捕捉任何状态,运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要每次用的时候于栈中创建。
另外,全局块的拷贝是个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。
例:
void (^block)() = &{
NSLog(@"This is a block");
};
由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。
要点
- 块是C、C++、Objective-C中的词法闭包。
- 块可接受参数,也可返回值。
- 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。
为常用的块类型创建typedef
每个块都具备其“固有类型”,因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。
例:
^(BOOL flag, int value) {
// Implementation
return someInt;
}
如果想要将其赋给变量,则需注意其类型。变量类型及相关赋值语句如下:
int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value) {
// Implementation
return someInt;
}
与其他类型的变量不同,在定义块变量时,要把变量名称放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型其个别名。
为例隐藏复杂的块类型,需要用的C语言中名为“类型定义”的特性。typedef关键字用于给类型起个易懂的别名。
例:
typedef int (^EOCSomeBlock)(BOOL flag, int vlaue);
上面这条语句向系统中新增了一个名为EOCSomeBlock的类型。此后,创建变量直接使用新类型:
EOCSomeBlock block = ^(BOOL flag, int value) {
// Implementation
};
块作方法参数
类里面有些方法可能需要使用块来做参数,遇到这种情况,可以通过定义别名使代码变得更为易读。
比方说,类里面又个方法,它接受一个块作为处理程序,在完成任务后执行这个块。若不定义别名,则方法签名会像下面这样:
- (void) startWithCompletHandler:(void (^)(NSData* data, NSError* error))completion;
这种情况,我们可以给参数类型起个别名,然后使用此名称来定义:
typedef void (^EOCCompletionHandler)(NSData *data, NSError *error);
- (void) startWithCompletHandler:(EOCCompletionHandler)completion;
当前,优秀的集成开发环境都可以自动吧类型定义展开,所以typedef这个功能变得很实用。
使用类型定义还有一个好处,就是当你打算重构块的类型签名时,只需要修改类型定义语句即可。
如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不能放入同一个继承体系,那么每个类就应该有自己的completion handler类型。这几个completion handler的签名也许完全相同,但最好还是在每个类里都各自定义一个别名。若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,以供个字类使用。
要点
- 以typedef重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef。