掌握GCD及操作队列的使用时机

在执行后台任务时,GCD 并不一定是最佳方式。还有一种技术叫做 NSOperationQueue,它虽然与 GCD 不同,但是却与之相关,开发者可以把操作以 NSOperation 子类的形式放在队列中,而这些操作也能够并发执行。

GCD是纯C的API,而NSOperationQueue是Objective-C的对象。这意味着使用GCD时,任务通过块(block)来表示,而块是一种轻量级的数据结构;而使用NSOperationQueue时,任务通过NSOperation的子类来表示,这是一种更为重量级的Objective-C对象。 虽然GCD提供了一种更轻量级的方式来处理任务,但并不总是最佳选择。有时候,使用NSOperationQueue所带来的开销微乎其微,而使用完整的对象所带来的好处可能会超过其缺点。NSOperationQueue提供了更多的灵活性和控制,例如可以对操作进行取消、暂停和恢复等操作。

NSOperationQueue相比于纯GCD的优势:

取消操作: 使用NSOperationQueue可以轻松取消操作。可以在NSOperation对象上调用cancel方法来设置取消标志,这使得取消操作变得更加简单。相比之下,如果使用纯GCD,任务一旦被提交到队列中就无法取消。指定操作间的依赖关系: NSOperation允许指定操作之间的依赖关系,这使得某些操作必须在其他操作执行完毕后才能执行。这种依赖关系对于需要按特定顺序执行任务的情况非常有用。监控操作属性: NSOperation对象的属性可以通过键值观察(KVO)机制进行监控,这使得可以轻松地检测操作的状态变化,例如判断操作是否被取消或完成。指定操作的优先级: NSOperation允许指定操作的优先级,这使得可以控制操作执行的顺序。与GCD不同,NSOperation提供了更为灵活的优先级管理机制。重用NSOperation对象: NSOperation对象是Objective-C的对象,可以存储任何信息,并且可以多次使用。这使得NSOperation相对于简单的GCD块更为强大,因为它们可以包含更多的逻辑和状态信息。

有一个 API 选用了操作队列而非派发队列,这就是 NSNotificationCenter ,开发者可通过其中的方法来注册监听器,以便在发生相关事件时得到通知,而这个方法接受的参数是块,不是选择子:

- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;

在解决多线程与任务管理问题时,派发队列并非唯一方案。操作队列提供了一套高层的 Objective-C API,能实现纯 GCD 所具备的绝大部份功能,而且还能完成一些更为复杂的操作,那些操作若改用 GCD 来实现,则需另外编写代码。

通过Dispatch Group机制,根据系统资源状况来执行任务

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 所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。

一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。通过 dispatch group ,可以在并发式派发队列里同时执行多项任务。此时 GCD 会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。

使用dispatch_once来执行只需运行一次的线程安全代码

单例模式常见的实现方式为:在类中编写名为 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 更高效。

经常需要编写 “只需执行一次的线程安全代码”(thread-safe single-code execution)。通过 GCD 所提供的 dispatch_once 函数,很容易就能实现此功能。标记应该声明在 statci 或 global 作用域中,这样的话,在把只需执行一次的块传给dispatch_once 函数时,传进去的标记也是相同的。

不要使用dispatch_get_current_queue

使用 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 的特定值,从而避免了循环等待。

dispatch_get_current_queue 函数对行为常常与开发者所预期的不同。此函数在iOS开发中已经废弃、只应做调试之用。由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用函数解决的问题,通常也能改用“队列特定数据”来解决。

熟悉系统框架

编写 Objective-C 应用程序时几乎都会用到系统框架,如果直接使用这些框架中的类,那么应用程序就可以得益于新版系统库所带来的改进,而开发者也就无须手动更新其代码了。 将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。

各种常用框架:

总结:

Cocoa 和 Cocoa Touch 框架:Cocoa 框架用于 macOS 应用程序开发,而 Cocoa Touch 用于 iOS 应用程序开发。它们集成了一系列常用的框架和工具,用于创建图形界面应用程序。Foundation 框架:Foundation 框架是 Cocoa 和 Cocoa Touch 中的一个核心框架,包含了诸如 NSObject、NSArray、NSDictionary 等基础类。它提供了许多基础核心功能,例如集合类、字符串处理以及字符串解析等。CoreFoundation 框架:CoreFoundation 框架不是 Objective-C 框架,而是用 C 语言编写的。然而,它与 Foundation 框架密切相关,提供了一套与 Foundation 中相对应的 C 语言 API。CoreFoundation 中的数据结构可以与 Foundation 中的 Objective-C 对象进行平滑转换,这种技术称为“无缝桥接”。CFNetwork 框架:此框架提供了C语言级别的网络通信能力,它将“BSD套接字”(BSD socket)抽象成易于使用的网络接口。而 Foundation 则将该框架里的部分内容封装为 Objective-C 语言的接口,以便进行网络通信,例如可以用 NSURLConnection 从 URL 中下载数据。CoreAudio 框架:该框架所提供的 C语言API 可用来操作设备上的音频硬件。这个框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套API 可以抽象出另外一套 Objective-C 式API,用后者来处理音频问题会更简单些。AVFoundation框架:此框架所提供的Objective-C 对像可用来回放并录制音频及视频,比如能够在 UI 视图类里播放视频。CoreData 框架:此框架所提供的 Objective-C 接口可将对象放入数据库,以便持久保存。CoreData 会处理数据的获取及存储事宜,而且可以跨越 Mac OS X 及 iOS 平台。CoreText 框架:此框架提供的C 语言接口可以高效执行文字排版及渲染操作。AppKit 和 UIKit 框架:AppKit 用于 macOS 应用程序开发,而 UIKit 用于 iOS 应用程序开发。它们是构建在 Foundation 和 CoreFoundation 之上的核心 UI 框架,提供了 UI 元素和粘合机制,用于组装应用程序的所有内容。CoreAnimation 框架:CoreAnimation 是一个用 Objective-C 编写的框架,它提供了渲染图形和播放动画所需的工具。虽然 CoreAnimation 本身不是一个框架,但它是 QuartzCore 框架的一部分,被广泛用于 UI 框架中。CoreGraphics 框架:CoreGraphics 是用 C 语言编写的框架,提供了绘制 2D 图形所需的数据结构和函数。UIKit 框架中的 UIView 类在确定视图控件之间的相对位置时会用到 CoreGraphics 中定义的数据结构。其他框架:除了上述核心 UI 框架之外,还有许多其他框架构建在 UI 框架之上,例如 MapKit 框架用于为 iOS 应用程序提供地图功能,Social 框架用于为 macOS 和 iOS 应用程序提供社交网络功能。这些框架通常与操作系统平台对应的核心 UI 框架结合使用,以丰富应用程序的功能。

可以看出Objective-C的一项重要特点:经常需要使用底层的 C 语言级 API。用 C 语言来实现 API 的好处是,可以绕过 Objective-C 的运行期系统,从而提升执行速度。

许多系统框架都可以直接使用。其中最重要的是 Foundation 与 CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。很多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。请记住:用纯C 写成的框架与用 Objective-C 写成的一样重要,若想成为优秀的 Objective-C 开发者,应该掌握C 语言的核心概念。

多用块枚举,少用for循环

语言中引入“块”这一特性后,又多出来几种新的遍历方式,采用这几种新方式遍历 collection 时,通常会大幅度简化编码过程,笔者下面将会详细说明。

for循环

for循环是大家都很熟悉的写法。但是用它遍历字典或者set就会很麻烦。因为字典与 set 都是“无序的”,所以无法根据特定的整数下标来直接访问其中的值。 for循环也可以执行反向遍历,执行反向遍历时,使用 for 循环会比其他方式简单许多。

使用 NSEnumerator 来遍历

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)连接,用以表明遍历方式。

遍历collection 有四种方法。最基本的办法是 for 循环,其次是 NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。“块枚举法”本身就能通过GCD 来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。若提前知道待遍历的 collection 含有何种对象,则应修改块签名,指出对象的具体类型。

对自定义其内存管理语义的collection使用无缝桥接

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,表示采用默认的分配器。第二个参数定义了字典的初始大小。它并不会限制字典的最大容量,只是向分配器提示了一开始应该分配多少内存。最后两个参数定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。

通过无缝桥接技术,可以在 Foundation 框架中的 Objective-C 对象与 CoreFoundation 框架中的 C 语言数据结构之间来回转换。在 CoreFoundation 层面创建collection 时,可以指定许多回调函数,这些函数表示此collection 应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的 Objective-C collection。

构建缓存时选用NSCache而非NSDctionary

实现缓存时使用NSCache 类更好,它是 Foundation 框架专为处理这种任务而设计的。。 NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。 NSCache 并不会“拷贝”键,而是会 “保留”它。NSCache 对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。 另外,NSCache 是线程安全的。而 NSDictionary 则绝对不具备此优势,意思就是:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问 NSCache。 开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”。开发者在将对象加入缓存时,可为其指定“开销值”。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。 向缓存中添加对象时,需要计算对象的“开销值”。这个“开销值”是一个附加因素,通常用于帮助决定何时从缓存中移除对象。

下面这段代码演示了缓存的用法:

  #import

  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 方法将访问结束。

实现缓存时应选用 NSCache 而非 NSDictionary 对象。因为 NSCache 可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”(hard limit),它们仅对NSCache 起指导作用。将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种 “重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

精简initialize与load的实现代码

有时候类必须先执行某些初始化操作,然后才能正常使用。-(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 方法只应该用来设置内部数据。不应该在其中调用其他方法,即便是本类自己的方法,也最好别调用。

在加载阶段,如果类实现了 load 方法,那么系统就会调用它。分类里也可以定义此方法,类的 load 方法要比分类中的先调用。与其他方法不同,load 方法不参与覆写机制。首次使用某个类之前,系统会向其发送 initialize 消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类。load 与 initialize 方法都应该实现的精简一些,这有助于保持应用程序的响应能力,也能减少引入 “保留环”(interdependency cycle)的几率。无法在编译期设定的全局常量,可以放在 initialize 方法里初始化。

别忘了NSTimer会保留其目标对象

计时器要和 “运行循环”(run loop)相关联,运行循环到时候会触发任务。创建 NSTimer 时,可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者自己来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。 由于计时器会保留其目标对象,所以反复执行任务通常会导致应用程序出问题。也就是说,设置成重复执行模式的那种计时器,很容易引入 “保留环”。 比如说下列代码:

  #import

  @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

@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 实例的最后一个引用将其释放,则该实例就可为系统所回收了。

NSTimer 对象会保留其目标,直到计时器本身失效为止,调用 invalidate 方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。反复执行任务的计时器(repeating timer),很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。可以扩充 NSTimer 的功能,用 “块”来打破保留环。不过,除非 NSTimer 将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。

参考文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。