文章目录
前言
开发应用程序时,最糟糕的事莫过于程序因UI线程阻塞而挂起了,在iOS系统中,阻塞过久可能会使应用程序终止执行,所幸苹果公司以全新的方式设计了多线程,并且当前多线程的核心就是“块”与“大中枢派发”,“块”是一种可在C、C++及OC代码中使用的“词法闭包”,GCD是一种与块有关的技术,它提供了对线程的抽象,而这种抽象则基于“派发队列”。
块与GCD都是当前OC编程的基石,因此,必须理解其工作原理及功能。
理解“块”这一概念
块可以实现闭包,并且其是作为“扩展”而加入GCD编译器中的。
块的基础知识
块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用“^”
符号来表示,后边跟着一对花括号,括号里面是块的实现代码。例如:
^{
//代码
}
实际定义和使用如:
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
}
int result = addBlock(2,5); //result = 7
那么这个count
值就可以在块中改变了,这也是“内联块”的用法。
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。这就引出了一个于块有关的重要问题。块本身可视为对象。并且块本身也和其他对象一样,有引用计数。
如果将块定义在OC类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self
变量。块总能修改实例变量,所以在声明时无需加_block
。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self
变量一并捕获了,因为实例变量是与self所指代的实例关联在一起的。也就是说,只要你在块中调用到了属性值,那么这个块就会捕获这个类本身也就是self
。
然而一定要记住:self
也是个对象,因而块在捕获它时也会将其保留。如果self
所指代的那个对象同时也保留了块,那么这种情况通常就会导致**“保留环”**。
块的内部结构
块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。块对象的内存布局:
- 在内存布局中,最重要的就是
invoke
变量,这是个函数指针,指向块的实现代码。函数原型至少需要接受一个void*
型的参数,此参数代表块。 escriptor
变量是指向结构体的指针,每个块里都包含此结构体,其中声明里块对象的总体大小,还声明里copy
与dispose
这两个辅助函数所对应的函数指针。- 块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在
descriptor
变量的后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke
函数为何需要把块对象作为参数传进来呢? 原因就在于,执行块时,要从内存中把这些捕获到的变量读出来。
全局块、栈块及堆块
定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。像下面这种代码就可能有危险:
void (^block) ();
if (/**/) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"B;ock B");
};
}
block();
- 定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存。然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。
- 为解决此问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放,而手动管理引用计数时则需要自己来调用release方法。当引用计数降为0后,“分配在堆上的块”(heap block)会像其他对象一样,为系统所回收。而“分配在栈上的块”(stackblock)则无须明确释放,因为栈内存本来就会自动回收,刚才那段范例代码之所以有危险,原因也在于此。
明白这一点后,我们只需给代码加上两个copy方法调用,就可令其变得安全了:
void (^block) ();
if (/**/) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"B;ock B");
} copy];
}
block();
现在代码就安全了。如果手动管理引用计数,那么在用完块后还需将其释放。
除了“栈块”和“堆块”之外,还有一类块叫做“全局块”(globalblock)。这种块不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。下面就是个全局块:
void (^block)() = ^{
NSLog(@"This is a block");
};
由于运行该块所需的全部信息都能在编译期确定,所以可以把它做成全局块。这完全是种优化技术:若把如此简单的块当作复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。
要点:
- 块是C、C++、OC中的词法闭包。
- 块可接受参数,也可返回值。
- 块可以分配在栈上或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的OC对象一样,具备引用计数了。
为常用的块类型创建typedef
每个块都具备其“固有类型”(inherent type),因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。例如下面这个块:
^(BOOL flag, int value) {
if (flag) {
return value * 5;
} else {
return value * 10;
}
}
此块接受两个类型分别为BOOL及int的参数,并返回类型为int的值。如果想把它赋给变量,则需注意其类型。变量类型及相关赋值语句如下:
int (^variableName)(BOOL flag, int value) =
^(BOOL flag, int value) {
// Implementation
return someInt;
}
这个类型似乎和普通的类型大不相同,然而如果习惯函数指针的话,那么看上去就会觉得眼熟了。块类型的语法结构如下:
return_type (^block_name)(parameters)
与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名,尤其是打算把代码发布成API供他人使用时,更应这样做。开发者可可以起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。
为了隐藏复杂的块类型,需要用到C语言中名为“类型定定义”(type definition)的特性。typedef关键字用于给类型起个易读的别名。比方说,想定义人新类型,用以表示接受BOOL及int参数并返回int值的块,可通过下列语句来做:
typedef int(</