Effective Objective-C 2.0(第6章)

编写高质量iOS与OS X代码的52个有效方法

第6章 块与大中枢派发

37.理解“块”这一概念

  • 块是C、C++、Objective-C 中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。

38.为常用的块类型创建 typedef

  • 以typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应的 typedef 中的块签名即可,无须改动其他typedef。

39.用 handler 块降低代码分散程度

  • 在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler 块来实现,则可直接将块与相关对象放在一起。
  • 设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

40.用块引用其所属对象时不要出现保留环

1
2
3
4
5
6
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}

只要下载请求执行完毕,保留环就解除了,而获取器对象也将会在必要时为系统所回收。

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给 API 的调用者。

41.多用派发队列,少用同步锁

  1. 同步块(synchronization block)

    1
    2
    3
    4
    5
    - (void)synchronizedMethod {
    @synchronized(self) {
    //Safe
    }
    }
  2. NSLock锁对象 NSRecursiveLock递归锁

    1
    2
    3
    4
    5
    6
    _lock = [[NSLock alloc] init];
    - (void)synchronizedMethod {
    [_lock lock];
    //Safe
    [_lock unlock];
    }

缺陷:

  • 在极端情况下,同步块会导致死锁。
  • 效率不见得很高。
  • 直接使用锁对象,一旦遇到死锁,就会非常麻烦。
  • 用atomic来修饰属性,只能提供某种程度的线程安全,无法保证访问该对象时绝对是线程安全的。使用属性时,必然能从中获取到有效值,然而在同一线程上多次调用获取方法(getter),每次获取到的结果未必相同。在两次访问操作之间,其他线程可能会写入新的属性值。
  1. GCD
    串行同步队列(serial synchronization queue)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    _syncQueue = dispatch_queue_create("com.effectiveObjectivec.syncQueue",NULL);
    - (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
    localSomeString = _someString;
    });
    return localSomeString;
    }
    - (void)setSomeString:(NSString *)someString {
    dispatch_sync(_syncQueue, ^{
    _someString = someString;
    });
    }

思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。全部加锁任务都在GCD中处理。
并发队列(concurrent queue)
栅栏(barrier)
在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块,那么久一直要等栅栏块执行过后,再按正常方式继续向下处理。

要点
  • 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized 块或 NSLock 对象更简单。
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
  • 使用同步队列及栅栏块,可以令同步行为更加高效。

42.多用GCD,少用 performSelector 系列方法

  1. performSelector 可能内存泄漏
    performSelector 调用了一个方法。编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
  2. 有局限性。
  • 返回值只能是void或对象类型。
  • 参数类型是id,所以只能传入对象。此外,最多只能接受两个参数。

dispatch_sync
dispatch_async
dispatch_after

要点
  • performSelector 系列方法在内存管理方面容易有疏忽。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
  • performSelector 系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都收到限制。
  • 如果想把任务放在另一个线程上执行,那么最好不要用 performSelector 系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。

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

GCD与NSOperationQueue
  • 操作队列在底层是用GCD来实现的。
  • GCD是纯C的API,而操作队列则是Objective-C的对象。
  • 在GCD中,任务用块来表示,而块是一个轻量级的数据结构。预支相反,“操作”(operation)则是个更为重量级的Objective-C对象。
使用NSOperation及NSOperationQueue的好处:
  • 取消某个操作。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需执行,不过,已经启动的任务无法取消。若是不使用操作队列,而是把块安排在GCD队列,那就无法取消了。开发者可以在应用程序层自己来实现取消功能,不过这样做需要编写很多代码,而那些代码其实已经由操作队列实现好了。
  • 指定操作间的依赖关系。
  • 通过键值观察机制监控NSOperation对象的属性。如isCancelled、isFinished。如果想在某个任务变更其状态时得到通知,或是想用比GCD更为精细的方式来控制所要执行的任务,那么键值观察机制会很有用。
  • 指定操作的优先级。GCD只有队列的优先级,没有任务的优先级。NSOpetation对象也有线程优先级,这决定了运行此操作的线程处在何种优先级上。GCD可以实现此功能,然而操作队列更简单,只需设置一个属性。
  • 重用NSOperation对象。
    系统的NSNotificationCenter API选用了操作队列而非派发队列,开发者可通过其中的方法来注册监听器,以便在发生相关事件时得到通知,而这个方法接受的参数是块,不是选择子。
要点
  • 在解决多线程与任务管理问题时,派发队列并非唯一方案。
  • 操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。

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

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

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

实现单例
1
2
3
4
5
6
7
8
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
要点
  • 经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的 dispatch_once 函数,很容易就能实现此功能。
  • 标记应该声明在 static 或 global 作用域中,这样的话,在把只需执行一次的块传给 dispatch_once 函数时,传进去的标记也是相同的。

46.不要使用 dispatch_get_current_queue

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

Effective Objective-C 2.0(第7章)

编写高质量iOS与OS X代码的52个有效方法第7章 系统框架47.熟悉系统框架将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。有时为iOS平台构建的第三方框架所使用的是静态库(static library),这是因为iOS应用程 …

Effective Objective-C 2.0 阅读全文
更早的文章

Effective Objective-C 2.0(第5章)

编写高质量iOS与OS X代码的52个有效方法第5章 内存管理29.理解引用计数 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。 在对象的生命周期中,其余对象通过引用来保留或释放此对象。保留与释 …

Effective Objective-C 2.0 阅读全文