在执行后台任务时,GCD 并不一定是最佳方式。还有一种技术叫做 NSOperationQueue,它虽然与 GCD 不同,但是却与之相关,开发者可以把操作以 NSOperation 子类的形式放在队列中,而这些操作也能够并发执行。
GCD是纯C的API,而NSOperationQueue是Objective-C的对象。这意味着使用GCD时,任务通过块(block)来表示,而块是一种轻量级的数据结构;而使用NSOperationQueue时,任务通过NSOperation的子类来表示,这是一种更为重量级的Objective-C对象。
虽然GCD提供了一种更轻量级的方式来处理任务,但并不总是最佳选择。有时候,使用NSOperationQueue所带来的开销微乎其微,而使用完整的对象所带来的好处可能会超过其缺点。NSOperationQueue提供了更多的灵活性和控制,例如可以对操作进行取消、暂停和恢复等操作。
NSOperationQueue相比于纯GCD的优势:
有一个 API 选用了操作队列而非派发队列,这就是 NSNotificationCenter ,开发者可通过其中的方法来注册监听器,以便在发生相关事件时得到通知,而这个方法接受的参数是块,不是选择子:
- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;
dispatch group(意为“派发分组”或“调度组”) 是 GCD 的一项特性,能够把任务分组。
其中最重要的用法,就是把将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。
把压缩一系列文件的任务表示成 dispatch group,下面这个函数可以创建 dispatch group:
dispatch_group_t dispatch_group_create();
想把任务编组,有两种办法。第一种是用下面这个函数:
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
它是普通 dispatch_async 函数的变体,比原来多一个参数,用于表示待执行的块所归属的组。还有种办法能够指定任务所属的 dispatch group,那就是使用下面这一对函数:
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
前者能够使分组里正要执行的任务数递增,而后者则使之递减。调用了 dispatch_group_enter 以后,必须有与之对应的 dispatch_group_leave 才行。这与引用计数相似。
下面这个函数可用于等待 dispatch group 执行完毕:
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
此函数接受两个参数,一个是要等待的 group,另一个是代表等待时间的 timeout 值。timeout 参数表示函数在等待 dispatch group 执行完毕时,应该阻塞多久。
除了可以用上面那个函数等待 dispatch group 执行完毕之外,也可以换个办法,使用下列函数:
void dispatch_group_notiy(dispath_group_t group, dispatch_queue_t queue, dispath_block_t block);
不同的是:开发者可以向此函数传入块,等 dispatch group 执行完毕之后,块会在特定的线程上执行。
比方说,在 Mac OS X 与 iOS 系统中,都不应阻塞主线程,因为所有 UI 绘制及事件处理都要在主线程上执行。如果想令数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用这个 GCD 特性来实现。
若当前线程不应阻塞,则可用 notify 函数来取代 wait:
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup, notifyQueue, ^{
//...
});
也可以把某些任务放在优先级高的线程上执行,同时仍然把所有任务都归入同一个 dispatch group。并在执行完毕时获得通知。
开发者未必总需要使用 dispatch group。有时候采用单个队列搭配标准的异步派发,也可以实现相同效果。
为了执行队列中的块,GCD 会在适当的时机自动创建新线程或复用旧线程。如果使用并发队列,那么其中有可能会有多个线程,这也意味着多个块可以并发执行。在并发队列中,执行任务所用的并发线程数量,取决于各种因素,而GCD 只要是根据系统资源状况来判定这些因素的。假如 CPU 有多个核心,并且队列中有大量任务等待执行,那么GCD 就可能会给该队列配备多个线程。通过 dispatch group 所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。
单例模式常见的实现方式为:在类中编写名为 sharedInstance 的方法,该方法只会返回全类共用的单例实例,而不会在每次调用时都创建新的实例。比如说:
@implementation EOCClass
+ (instancetype)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
@end
不过,GCD 引入了一项特性,能使单例实现起来更为容易。所用的函数是:
void dispatch_once(dispatch_once_t *token, dispatch_block_t block);
此函数接受类型为 dispatch_once_t 的特殊参数,笔者称其为 “标记”(token),此外还接受块参数。对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行一次。此操作完全是线程安全的。
刚才实现单例模式所用的 sharedInstance 方法,可以用此函数来改写:
+ (instancetype)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
使用 dispatch_once 可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁或同步。所有问题都由 GCD 在底层处理。由于每次调用时都必须使用完全相同的标记,所以标记要声明成 static。此外,dispatch_once 更高效。
使用 GCD 时,经常需要判断当前代码正在哪个队列上执行,向多个队列派发任务时,更是如此。
dispatch_get_current_queue本来是用于解决由不可重入的代码所引发的死锁,但是因为它已经被废弃,所以可以选择通过 GCD 所提供的功能来设定“队列特有数据”(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达根队列为止。比如说这个例子:
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ NSLog(@"No deadlock!"); };
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
} else {
dispatch_sync(queueA, block);
}
});
在上面这段代码中:有两个串行队列 queueA 和 queueB。将 queueB 的目标队列设置为 queueA,表示 queueB 依赖于 queueA。定义一个静态整型变量 kQueueSpecific 作为键值,用于关联队列特有数据。使用 dispatch_queue_set_specific 函数将特定值(在这里是 queueA 的标识符)与 queueA 关联起来。在 queueB 上执行同步任务,内部判断是否可以访问 queueA 的特定值。如果能够访问到 queueA 的特定值,则直接执行任务,否则在 queueA 上同步执行任务。
这样,即使在 queueB 上同步执行任务,也不会产生死锁,因为在获取 queueA 的特定值时,会自动向上查找到其父级队列 queueA 的特定值,从而避免了循环等待。
编写 Objective-C 应用程序时几乎都会用到系统框架,如果直接使用这些框架中的类,那么应用程序就可以得益于新版系统库所带来的改进,而开发者也就无须手动更新其代码了。
将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。
各种常用框架:
总结:
可以看出Objective-C的一项重要特点:经常需要使用底层的 C 语言级 API。用 C 语言来实现 API 的好处是,可以绕过 Objective-C 的运行期系统,从而提升执行速度。
语言中引入“块”这一特性后,又多出来几种新的遍历方式,采用这几种新方式遍历 collection 时,通常会大幅度简化编码过程,笔者下面将会详细说明。
for循环是大家都很熟悉的写法。但是用它遍历字典或者set就会很麻烦。因为字典与 set 都是“无序的”,所以无法根据特定的整数下标来直接访问其中的值。
for循环也可以执行反向遍历,执行反向遍历时,使用 for 循环会比其他方式简单许多。
NSEnumerator 是个抽象基类,其中只定义了两个方法,供其具体子类(concrete subclass)来实现:
- (NSArray *)allObjects;
- (id)nextObject;
其中关键的方法是 nextObject,它返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。等到枚举中的全部对象都已返回之后,再调用就将返回 nil,这表示达到枚举末端了。
例如遍历字典和set:
// Dictionary
NSDictionary *aDictionary = /*...*/;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil) {
id value = aDictionary[key];
// Do something with 'key' and 'value'
}
// set
NSSet *aSet = /*...*/;
NSEnumerator *enumerator = [aSet objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
// Do something with 'object'
}
在第一段代码中,使用了 NSDictionary 类的 keyEnumerator 方法来获取字典中所有键的枚举器,然后通过枚举器逐个获取键,并使用键来访问字典中的值。在注释部分的代码块中,可以对每个键值对执行相应的操作。
在第二段代码中,使用了 NSSet 类的 objectEnumerator 方法来获取集合中的所有对象的枚举器,然后通过枚举器逐个获取集合中的对象。在注释部分的代码块中,可以对每个对象执行相应的操作。
快速遍历与使用NSEnumerator 来遍历差不多,然而语法更简洁,它就是for…in…
这样写简单多了。如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为 NSFastEnumeration 的协议,从而令开发者可以采用此语法来迭代该对象。
遍历字典与set 也很简单:
// Dictionary
NSDictionary *aDictionary = /*...*/;
for (id key in aDictionary) {
id value = aDictionary[key];
// Do something with 'key' and 'value'
}
// Set
NSSet *aSet = /*...*/;
for (id object in aSet) {
// Do something with 'object'
}
由于 NSEnumerator 对象也实现了 NSFastEnumeration 协议,所以能用来执行反向遍历。若要反向遍历数组,可采用下面这种写法:
NSArray *anArray = /*...*/;
for (id object in [anArray reverseObjectEnumerator]) {
// Do something with 'object'
}
最新引入的一种做法就是基于块来遍历。NSArray 中定义了下面这个方法,它可以实现最基本的遍历功能:
- (void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx, BOOL *stop))block;
这个方法的方法名是enumerateObjectsUsingBlock:,它的参数:一个块作为参数,块包含三个参数:object:数组中的元素;idx:元素在数组中的索引;stop:一个指向布尔值的指针,用于控制遍历过程。如果将 *stop 设置为 YES,则遍历会停止。
该方法遍历时既能获取对象,也能知道其下标。此方法还提供了一种优雅的机制,用于终止遍历操作。
用它遍历字典与 set 也同样简单:
// Dictonary
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeyAndObjectsUsingBlock:
^(id key, id object, BOOL *stop){
// Do something with 'key' and 'object'
if (shouldStop) {
*stop = YES;
}
}];
// Set
NSSet *aSet = /*...*/;
[aSet enumerateObjectsUsingBlock:
^(id object, BOOL *stop) {
// Do something with 'object'
if (shouldStop) {
*stop = YES;
}
}];
此方式大大胜过其他方式的地方在于:遍历时可以直接从块里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序set (NSOrderedSet)时也一样。而在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。
另外一个好处是,能够修改块的方法签名,以免进行类型转换操作。
若已知字典中的对象必为字符串,则用基于块的方式来遍历可以这样编码:
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeysAndObjectsUsingBlock:
^(NSString *key, NSString *obj, BOOL *stop) {
// Do something with 'key' and 'obj'
}];
之所以能如此,是因为 id 类型相当特殊,它可以像本例这样,为其他类型所覆写。
用此方式也可以执行反向遍历。数组、字典、set 都实现了前述方法的另一个版本,使开发者可向其传入 “选项掩码”(option mask):
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id key, id obj, BOOL *stop))block
NSEnumerationOption 类型是个 enum,其各种取值可用“按位或”(bitwise OR)连接,用以表明遍历方式。
CoreFoundation 框架也定义了一套C 语言API,用于操作表示这些collection 及其他各种collection 的数据结构。例如,NSArray 是Foundation 框架中表示数组的 Objective-C 类,而CFArray 则是 CoreFoundation 框架中的等价物。这两种创建数组的方式也许有区别,然而有项强大的功能可在这两个类型之间平滑转换,它就是 “无缝桥接”(toll-free bridging)。
使用“无缝桥接”技术,可以在定义于 Foundation 框架中的 Objective-C 类和定义于 CoreFoundation 框架中的C数据结构之间相互转换。
下列代码演示了简单的无缝桥接:
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (_bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
// Output: size of array = 5
这段代码演示了如何将NSArray对象转换为CFArrayRef类型,使用__bridge进行类型转换,将anNSArray转换为CFArrayRef类型的对象aCFArray。
__bridge 本身的意思是:ARC 仍然具备这个 Objective-C 对象的所用权。而 __bridge_retained 则与之相反,意味着ARC 将交出对象的所有权。与之相似,反向转换可通过 __bridge_transfer 来实现。这三种转换方式称为 “桥式转换”。
在使用 Foundation 框架中的字典对象时会遇到一个大问题,那就是其键的内存管理语义为 “拷贝”,而值的语义是却是“保留”。除非使用强大的无缝桥接技术,否则无法改变其语义。
CoreFoundation 框架中的字典类型叫做 CFDictionary。其可变版本称为 CFMutableDictionary 。创建 CFMutableDictionary 时,可以通过下列方法来指定键和值的内存管理语义:
CFMutableDictionaryRef CFDictionaryCreateMutable (
CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
}
首个参数表示将要使用的内存分配器。CoreFoundation 对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存。开发者通常为这个参数传入 NULL,表示采用默认的分配器。第二个参数定义了字典的初始大小。它并不会限制字典的最大容量,只是向分配器提示了一开始应该分配多少内存。最后两个参数定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。
实现缓存时使用NSCache 类更好,它是 Foundation 框架专为处理这种任务而设计的。。
NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。
NSCache 并不会“拷贝”键,而是会 “保留”它。NSCache 对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。
另外,NSCache 是线程安全的。而 NSDictionary 则绝对不具备此优势,意思就是:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问 NSCache。
开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”。开发者在将对象加入缓存时,可为其指定“开销值”。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。
向缓存中添加对象时,需要计算对象的“开销值”。这个“开销值”是一个附加因素,通常用于帮助决定何时从缓存中移除对象。
下面这段代码演示了缓存的用法:
#import <Foundation/Foundation.h>
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
@interface EOCClass : NSObject
@end
@implementation EOCClass {
NSCache *_cache;
}
- (id)init {
if ((self = [super init])) {
_cache = [NSCache new];
_cache.countLimit = 100;
/**
* The Size in bytes of data is used as the cost,
* so this sets a cost limit of 5MB.
*/
_cache.totalCostLimit = 5 * 1024 * 1024;
}
return self;
}
- (void)downloadDataForURL:(NSURL *)url {
NSData *cachedData = [_cache objectForKey:url];
if (cachedData) {
// Cache hit
[self useData:cacheData];
} else {
// Cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
[_cache setObject:data forKey:url cost:data.length];
[self useData:data];
}];
}
}
@end
这段代码实现了一个网络数据下载器 EOCNetworkFetcher 和一个使用了缓存的类 EOCClass。
EOCNetworkFetcher 类:负责从指定的 URL 下载数据。它包含了一个初始化方法 initWithURL: 和一个启动下载的方法 startWithCompletionHandler:。
EOCClass 类:包含了一个 NSCache 对象 _cache,用于缓存下载的数据。它还有一个方法 downloadDataForURL:,用于下载指定 URL 的数据,首先检查缓存中是否有数据,如果有则直接使用缓存的数据,如果没有则启动网络下载器进行下载,并将下载的数据存入缓存中。
在本例中,下载数据所用的 URL,就是缓存的键。若缓存中没有访问者所需的数据,则下载数据并将其放入缓存。
还有个类叫做 NSPurgeableData,此类是NSMutableData 的子类,而且实现了 NSDiscardableContent 协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。
如果将 NSPurgeableData 对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过 NSCache 的 evictsObjectsWithDiscardedContent 属性,可以开启或关闭此功能。
使用 NSPurgeableData 改写的话:
- (void)downloadDataForURL:(NSURL *)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) {
[cacheData beginContentAccess];
[self useData:cachedData];
[cacheData endContentAccess];
} else {
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
[_cache setObject:purgeableData forKey:url cost: purgeableData.length];
[self useData:data];
[purgeableData endContentAccess];
}];
}
}
这段代码通过 _cache 对象使用给定的 url 从缓存中获取数据。这里使用了 NSPurgeableData 类型的 cachedData 对象来存储获取到的数据。
如果缓存中存在数据(即 cachedData 不为 nil),则进入缓存命中的分支。在这个分支中,首先通过 beginContentAccess 方法将数据标记为开始访问状态,然后使用该数据,最后通过 endContentAccess 方法将访问结束。
如果缓存中不存在数据,则进入缓存未命中的分支。在这个分支中,首先创建一个 EOCNetworkFetcher 对象,用于从网络下载数据。在下载完成后,将下载的数据封装为 NSPurgeableData 对象,并存入缓存中。然后使用该数据,最后通过 endContentAccess 方法将访问结束。
有时候类必须先执行某些初始化操作,然后才能正常使用。-(void) load 方法,它是一个类方法,用于在类或分类被加载到运行时系统时执行。每个类和分类都会在程序启动时调用一次 load 方法,且仅调用一次。
load 方法的执行顺序是先执行类的 load 方法,然后执行分类的 load 方法。这意味着,如果一个类有多个分类,并且它们都实现了 load 方法,那么先执行类的 load 方法,然后按照分类的引入顺序依次执行各个分类的 load 方法。
然而,load 方法存在一个问题,即在执行该方法时,运行时系统处于“脆弱状态”。这意味着在 load 方法中使用其他类可能是不安全的,因为在执行子类的 load 方法之前,必定会先执行所有超类的 load 方法。而在加载依赖的其他库时,无法确定其中各个类的加载顺序,因此在 load 方法中使用其他类是不可靠的。
比如说:
#import "EOCClassA.h"
@implementation EOCClassB
+ (void)load {
NSLog(@"Loading EOCClassB");
EOCClassA *object = [EOCClassA new];
}
在 EOCClassB 的load 方法里使用 EOCClassA 却不太安全,因为无法确定在执行 EOCClassB 的 load 方法之前,EOCClassA 是不是已经加载好了。
load 方法不像普通的方法一样遵循继承规则。如果一个类自身没有实现 load 方法,那么无论其超类是否实现了 load 方法,系统都不会自动调用。这意味着子类的 load 方法不会自动调用其父类的 load 方法,除非子类自己实现了 load 方法并在其中显式调用了父类的 load 方法。
而且 load 方法务必实现得精简一些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行load 方法时都会阻塞。
想执行与类相关的初始化操作,还有个办法,就是覆写下列方法:
+ (void)initialize;
对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。 它是由运行期系统来调用的,绝不应该通过代码直接调用。只有当程序用到了相关的类时,它才会调用。
此方法与 load 还有个区别,就是运行期系统在执行该方法时,是处于正常状态的,因此,从运行期系统完整度上来讲,此时可以安全使用并调用任意类中的任意方法。
最后一个区别是: initialize 方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。即它是可以继承的。
当初始化基类 EOCBaseClass 时,EOCBaseClass 中定义的 initialize 方法要运行一遍,而当初始化 EOCSubClass 时,由于该类并未覆写此方法,因而还要把父类的实现代码再运行一遍。鉴于此,通常都会这么来实现 initialize 方法:
+ (void)initialize {
if (self == [EOCBaseClass class]) {
NSLog(@"%@ initialized", self);
}
}
load 与 initialize 方法的实现代码要尽量精简。在里面设置一些状态,使本类能够正常运作就可以了,不要执行那种耗时太久或需要加锁的任务。对于 load 方法来说,其原因已在前面解释过了,而 initialize 方法要保持精简的原因,也与之相似。
其二,开发者无法控制类的初始化时机。类在首次使用之前,肯定要初始化,但编写程序时不能令代码依赖特定的时间点,否则会很危险。
最后一个原因,如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。
initialize 方法只应该用来设置内部数据。不应该在其中调用其他方法,即便是本类自己的方法,也最好别调用。
计时器要和 “运行循环”(run loop)相关联,运行循环到时候会触发任务。创建 NSTimer 时,可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者自己来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。
由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出问题。也就是说,设置成重复执行模式的那种计时器,很容易引入 “保留环”。
比如说下列代码:
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation EOCClass {
NSTimer *_pollTimer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
[_pollTimer invalidate];
}
- (void)stopPolling {
[_pollTimer invalidate];
_pollTimer = nil;
}
- (void)startPolling {
_pollTimer = [NSTimer scheduledTimerWithTimeInterval: 5.0 target: self selector:@selector(p_doPoll) userInfo: nil repeats: YES];
}
- (void)p_doPoll {
}
@end
这段代码有个问题:当创建 EOCClass 实例并调用其 startPolling 方法时,会创建一个 NSTimer 对象并将其赋值给 _pollTimer 实例变量。由于 NSTimer 对象的目标对象是 EOCClass 实例本身,因此会对 EOCClass 实例进行强引用,导致 EOCClass 实例无法被释放。而 _pollTimer 实例变量也会对 NSTimer 对象进行强引用,使得 NSTimer 对象也无法被释放。这样就形成了一个保留环,即相互持有对方的强引用,导致内存泄漏。
如果想在系统回收本类实例的过程中令计时器无效,从而打破保留环,那又会陷入死结。因为在计时器对象尚且有效时,EOCClass 实例的保留计数绝不会降为 0 ,因此系统也绝不会将其回收。而现在又没人来调用 invalidate 方法,所以计时器将一直处于有效状态。
当指向 EOCClass 实例的最后一个外部引用被移除后,该实例仍然存活,因为计时器持有对它的强引用。同时,计时器对象也无法被系统释放,因为它被 EOCClass 实例强引用。这导致了实例和计时器对象互相持有对方的强引用,形成了保留环,导致内存泄漏。
更糟糕的是,除了计时器之外,已经没有其他引用指向 EOCClass 实例了,因此该实例会永远被保留,无法被释放。
这个问题可通过“块”来解决。虽然计时器当前并不直接支持块,但是可以用下面这段代码为其添加此功能:
#import <Foundation/Foundation.h>
@interface NSTimer (EOCBlockSupport)
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)eoc_blockInvoke:(NSTimer *)timer {
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
这段代码为 NSTimer 添加了支持块(blocks)的功能。通过类别(category),扩展了 NSTimer 类,添加了一个类方法 eoc_scheduledTimerWithTimeInterval:block:repeats:,用于创建带有块回调的定时器。
在 eoc_scheduledTimerWithTimeInterval:block:repeats: 方法中,调用了 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 方法创建了一个定时器,并传入了一个 selector,但是这个 selector 实际上是 eoc_blockInvoke: 方法。同时,将传入的块对象通过 copy 方法复制,并作为 userInfo 参数传递给定时器。
eoc_blockInvoke: 方法是一个类方法,用于实际执行定时器触发时的回调操作。它从定时器的 userInfo 中获取了保存的块对象,并执行该块对象。
这样,通过调用 eoc_scheduledTimerWithTimeInterval:block:repeats: 方法创建的定时器,当触发定时器时,就会执行传入的块代码块。
我们修改刚才那段有问题的范例代码,使用新分类中的 eoc_scheduledTimerWithTimeInterval 方法来创建计时器并改用 weak 引用,即可打破保留环:
- (void)startPolling {
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval: 5.0 block:^{
EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
}
这段代码采用了一种很有效的写法,它先定义了一个弱引用,令其指向 self,然后使块捕获这个引用,而不直接去捕获普通的 self 变量,也就是说,self 不会为计时器所保留。当块开始执行时,立刻生成 strong 引用,以保证实例在执行期间持续存活。
采用这种写法之后,如果外界指向 EOCClass 实例的最后一个引用将其释放,则该实例就可为系统所回收了。