具体Blocks底层的学习可以参考这篇博客:【iOS开发】—— 一文搞懂blocks底层源码
当前多线程编程的核心就是“块”与“大中枢派发”。GCD
是一种与块相关的技术,它提供了对线程的抽象,而这种抽象则基于“派发队列”(dIspatch queue
)。开发者可将块排入到派发队列中,由GCD
负责处理所有的调度事宜。
“块”是一种可在C
、C++
以及Objective-C
代码中使用的“词法闭包”,借助它,开发者可以将代码像对象一样传递,令其在不同环境下运行。还有个关键的地方,在定义“块”的范围内,它可以访问到其中的全部变量。此项语言特性是作为“扩展”而加入GCC编译器中的。
块的形式:
^{
//Block implementation here
}
块其实是个值,有自己的相关类型,可以赋值等操作。
块和对象一样,也适用ARC管理机制。
块将块定义在Objective-C
实例方法中,除了可以访问类的所有实例变量之外,还可以捕获self
变量,因为实例变量和self所指代的实例关联在一起。
直接访问实例变量和通过self
访问实例变量是等效的。通过属性访问实例变量,需要指明self
。
self也是一个对象,因而在块捕获它时;如果self所指代的那个对象同时也保留了块,就有可能形成“保留环”。
Class
对象的指针,该指针叫做isa。invoke
变量,这是一个函数指针,指向块的实现代码。其原型至少要接受一个void*型的参数,此参数代表块。descriptor
变量是指向结构体的指针,每个块里都有此结构体,声明了块对象的总体大小,还声明了copy
和dispose
这两个辅助函数所对应的函数指针。辅助函数是在拷贝及丢弃块对象时运行。descripto
r变量的后面。拷贝的不是这些对象本身,而是指向这些对象的指针变量。定义块时,其所占的内存区域是分配在栈上,所以超出它的定义范围会被销毁。为解决此问题,可以利用copy
方法,拷贝到堆上,成为堆块,这样就可以利用ARC机制了。除了以上两种类型的块,还有一种全局块。这种块不会捕获任何状态,且这种块所使用的内存区域,在编译器已经完全确定。因此,声明全局块可以声明在全局内存中,而不需要在每次操作的时候在栈中创建,另外,全局块的copy是空操作,所以,全局块绝对不会被系统所回收。这种块其实相当于单例。
为了隐藏复杂的块类型,所以利用typedef
关键字用于给类型起个易读的别名。
typedef return_type(^BlockName)(parameters)
这样定义之后,下次就可以直接使用:
BlockName block = ^(parameters){
//Implementation
}
typedef
重新定义块类型,可令块变量用起来更加简单。typedef
中的块签名即可,无须改动其他typedef
。为用户界面编码时,一种常用的范式就是“异步执行任务”。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。
异步方法在执行完任务时,需要以某种手段通知相关代码,实现此功能的方法很多,常用的一个设计一个委托协议,令关注此事件的对象遵从该协议。但是使用委托协议有很多缺点:
delegate
回调方法里根据传入的获取器参数来切换。所以我们改用块来写,有时由于成功和失败的情况要分开处理,所以就可以像下面这么写:
基于handler来设计API还有个原因,就是某些代码必须运行在特定的线程上,比方说Cocoa
与Cocoa Touch
中的UI操作必须在主线程上执行。这就相当于GCD
中的“主队列”。因此,最好能由调用API的人来决定handler
应该运行在哪个线程上。这里可以借助NSNotificationCenter
来实现。
handler
块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。举个栗子来说明:下面这个类提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时,可设置completion handler
,这个块会在下载结束之后以回调方式执行。为了能在下载完成后通过p_requestCompleted
方法执行调用这所指定的块,这段代码需要把completion handler保存到实例变量中:
仔细看这段代码,就会发现其中存在一个保留环,因为completion handler块要设置_fetchedData实例变量,所以它要捕获self变量,这就是说handler保留了创建网络数据获取器的那个EOCClass实例,但是EOCClass又通过strong实例变量保留了获取器,最后获取器对象又保留了handler块。
将p_requestCompleted修改成下面这样就可以了:
在Objective-C中,如果有多个线程要执行同一份代码,那么有时就可能出问题。这种情况下就要使用锁来实现某种同步机制。在GCD出现之前,有两种方法,一种是采用内置的“同步块”:
- (void)synchronizedMethod {
@synchronized(self) {
//safe
}
}
这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码的结尾处,锁就释放了。但是若是频繁使用锁,会降低代码效率。因为共用同一个锁的那些同步块,都必须按顺序执行。
另外一个方法是直接使用NSLock
对象:
_lock = [[NSLock alloc] init];
- (void)synchronizedMthod {
[_lock lock];
//Safe
[_lock unlock];
}
也可以使用NSRecursiveLock
这种“递归锁”,线程能够多次持有该锁,而不会出现死锁现象。
这两种方法都很好,但是也有缺陷。比方说,在极端的情况下,同步块会导致死锁,另外其效率也不会很高。
所以就有了GCD来更简单更高效的形式为代码加锁。使用“串行同步队列”。将读取操作及写入操作都安排在同一个队列中,即可保证数据同步。
其用法如下:
此模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作都同步了。
可以进一步优化,将设置方法改为:
这次将同步派发改为了异步派发,可以提升设置方法的执行速度。执行异步派发时,需要拷贝块,若拷贝块所花费的时间明显超过执行块说花的时间,则这种做法将比原来的慢。
多个获取方法可以并发执行,而获取方法与设置方法不能并发执行,利用这个特点,就可以体现出GCD的好处了。这次不用串行并列,而改用并发队列:
但是只是这样还无法正确实现同步。所有读写操作和写入操作都会在同一个队列上执行,不过由于并发队列,所以读取和写入操作都是随意执行的,而我们并不想让这些操作随意执行,此问题用一个简单的GCD功能来解决,就是“栅栏”。下列函数可以向队列中派发块,将其作为栅栏使用:
在队列中,栅栏必须单独执行,不能与其他块并行。
在本例中,可以用栅栏块来实现属性的设置方法,之后对属性的读取操作依然可以并发执行,但是写入操作却必须单独执行。实现代码如下:
@synchronized
块或 NSLock
对象更简单。通过performSelector:调用方法,会程序发出内存泄露的警示信息。原因在于,编译器只有在运行期时才能确定选择子,所以编译器并不知道将要调用的选择子是什么,所以就没办法运用ARC的内存管理规则来判定返回的值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
使用performSelector方法的弊端:
所以采用替代方案,最主要的替代方案就是使用块。
例如要延后执行某项任务,可以有下面两种实现方式:
优先考虑第二种。
若想把任务放在主线程上执行,也可以有下面两种形式:
优先考虑后者。
performSelector
系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。performSelector
系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。performSelector
系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。在执行后台任务时,GCD不一定是最佳方式,还有一种技术叫做NSOperationQueue
,开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作可以并发执行。
两者的区别
GCD是纯C的API,而操作队列则是Objective-C的对象。在GCD中,任务用块来解释,而块是个轻量级数据结构。与之相反,“操作”则是个更为重量级的Objective-C对象,虽说如此,但GCD并不总是最佳方案,有时采用对象所带来的的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。
用NSOperationQueue
类的“addOperationWithBlock:”方法搭配NSBlockOperation类来使用操作队列。
Objective-C API
,能实现纯GCD所具备的绝大部分功能而且还能完成一些更为复杂的操作,那些操作若改用GCD 来实现,则需另外编写代码。Dispatch Group
是GCD的一种特性,能够把任务分组。
为任务分组有下面两种方法:
第一种:
它是普通dispatch_async函数的变形,比原来多一个参数,用于表示待执行块所归属的组。
还有种方法能指定任务所属的dispatch group
:
前者能使分组里的任务数递增,后者能使之递减。调用了前者之后,必须有与之对应的后者才行。
下面这个函数可用于等待dispatch group
执行完毕:
次函数要接受两个参数,一个是等待的group,另一个是代表等待时间的timeout
值。timeout
参数表示函数在等待dispatch group
执行完毕时,应该阻塞多久。如果执行dispatch group
所需的时间小于timeout
,则返回0,否则返回非0值。此参数也可以取常量DISPATCH_TIME_FOREVER
,这表示函数会等待dispatch group执行完毕完,而不会超时。
还可以换个方法:
开发者可以向此函数传入块,等dispatch group
执行完毕后,块会在特定的线程上执行。
假如把块派给了当前队列(或者体系中高于当前队列的某个串行队列),就会导致死锁。若想在后台执行任务,则应该使用dispatch group
。
dispatch group
之中。开发者可以在这组任务执行完毕时获得通知。GCD引入了一项特性,能使单例实现起来更为容易。所用的函数是:
该函数的参数中有一个是dispatch_once_t
的特殊参数,这个要将其声明在stastic或者
global
作用域里。
首次调用该函数时,必然会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。
dispatch_once
函数,很容易就能实现此功能。static
或global
作用域中,这样的话,在把只需执行一次的块传给 dispatch_once
函数时,传进去的标记也是相同的。dispatch_get_current_queue
函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。dispatch_get_current_queue
函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。