type
status
date
slug
summary
tags
category
icon
password
这是Effective Objective-C 2.0系列的第6篇。

一、最佳实践

  • 用handler Block降低代码分散程度
  • 在创建对象时,可以使用内联的handler Block将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler Block来实现,则可直接将Block与相关对象放在一起;
  • 设计API时,如果遇到handler Block,那么可以新增一个参数,使调用者可以通过该参数来决定应该把Block安排在哪个队列上执行。
  • 使用Block中发生的循环引用要避免
  • 多用派发队列,少用同步锁
  • 派发队列可用来表述同步语义,这种做法要比使用@synchronizedNSLock对象更简单;
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做不会阻塞执行异步派发的线程;
  • 使用同步队列及栅栏块,可以令同步行为更高效;
  • 多用GCD,少用performSelector系列方法
  • performSelector系列方法在内存管理易有疏漏,它无法确定将要执行的选择子具体是什么,所以ARC编译器也就无法插入适当的内存管理方法;
  • performSelector系列方法所能处理的选择子太过局限,选择子返回值类型及发送方给方法的参数个数都受到限制;
  • 如果想延迟执行,最好不要用performSelector系列方法,而是应该把任务封装到Block里,调用GCD来实现。
  • 掌握GCD及操作队列的使用时机
  • 取消某个操作;
  • 指定操作间的依赖关系;
  • 通过键值观察机制监控NSOperationNSOperation对象许多属性都适合通过键值观察机制来监听,比如isCancelledisFinished
  • 指定操作的优先级;
  • 使用dispatch_once来执行只需运行一次的线程安全代码
  • 不要使用dispatch_get_current_queue
  • 通过Dispatch Group机制,根据系统资源状况来执行任务
  • 一系列的任务可归入一个dispatch_group中,开发者可以在这组任务完毕时获得通知;
  • 通过dispatch_group,可以在并发时派发队列同时执行多项任务。

二、实践详解

2.1 理解Block这一概念

Block用“^”(脱字符或插入符)来表示:
Block其实是个值,自有其类型,与int、float或Objective-C对象一样,也可以把Block赋给变量,其与函数指针类似。 Block的完整的语法结构如下:
看一个实例:
调用:
下面是各种情况下的Block的写法:
Block的强大之处:在声明它的范围里,所有变量都可以为其所捕获。就是Block里可以用该范围的所有变量。
如果需要修改Block所捕获的变量,需要加上__block。

2.1.1 函数指针

为了更好说明Block,这里说明下函数指针。
函数指针是指向函数的指针变量。 因而“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数的参数。下面是个实例:

2.1.2 Block的内部结构

Block本身是个对象,在存放Block的内存区域里,第一个个变量是指向Class对象的指针,该指针叫做isa,其余内存里含有对象正常运转所需的各种信息:
notion image
  • Impl 是个结构体。内部有个FuncPtr指向Block的实现代码,此参数代表Block。Block实现了把原来标准C语言中需要“不透明的void指针”传递状态变的透明,而且简单易用。
  • descriptor是指向结构体的指针,每个Block都包含该结构体。其中声明了copy及dispose这两个辅助函数所对应的函数指针。辅助函数在Block拷贝或者丢弃Block对象是运行。
  • size:Block的大小;
  • copy:辅助函数,保留捕获的对象;
  • dispose:辅助函数,释放捕获的对象;
  • Block会将其所捕获的所有变量都拷贝一份,置于descriptor之后,要注意的是,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke函数为何需要把Block作为对象参数传进来呢?原因在于,执行Block时,要从内存中把这些捕获到的变量读出来。

2.1.3 全局Block、栈Block和堆Block

定义Block时,其所占的内存区域是分配在栈中的,即Block只在定义它的那个范围内有效。如:
if/else中定义的Block,都是在栈中,当离开了相应的范围后,该栈内存有可能会被覆写。所以在运行时,有可能正确运行,也有可能发生崩溃。这取决于编译器是否覆写了该Block内存。
栈内存中的Block对象,无需考虑对象的释放,因为栈内存是系统管理的,系统会保证回收对象。
为了解决该问题,可以给Block对象发送copy消息,以执行拷贝。就可把Block对象从栈内存拷贝到堆内存。
堆内存中的Block对象,同普通对象一致,有引用计数,拷贝是递增引用计数,在ARC时无需手动释放,在引用计数为0时自动释放等。
除了上面的“栈Block”和“堆Block”,还有一类叫做“全局Block”。全局Block,有下面几个特点:
  • 不会捕捉任何状态,比如外围的变量等,运行时也无需有状态来参与;
  • Block所使用的是整个内存区域,在编译器已经完全确定,因此全局Block可以声明在全局内存里,而不需要每次用到的时候在栈中创建;
  • 全局Block的拷贝操作是个空操作,因为全局Block决不可能为系统所回收;
  • 全局Block相当于单例;下面是个全局Block:
由于运行该Block所需的全部信息都能在编译器确定,所以可把它做成全局Block,这完全是种优化技术。若把如此简单的Block当做复杂的Block来处理,那就会在复制或者丢弃该Block执行一些无谓的操作。

2.2 用handler Block降低代码分散程度

委托代理能很大程度上实现异步回调处理这样的事,但是委托代理这种模式却会使得代码极度分散。
用handler来集中代码,是个不错的选择。
风格一:代码易懂,将成功与失败的逻辑分开来写,必要时可以忽略成功或者失败的处理情形。
风格二:
  • 缺点:需要检测error,且全部逻辑都在一起,可能会令Block比较长,且比较复杂。
  • 优点:更为灵活,比如数据下载到一半时,网络故障,此时可以把数据即相关的错误传给Block,以便保存已下载数据及对错误进行处理。另外一个优点就是,调用API的代码可能会在处理处理成功的响应过程中发现错误。此时可以把成功中的错误处理同真正的错误一并处理,而不会造成代码冗余。假如分开处理,那么就会有两份一样的错误处理代码,而进一步,抽取成公共方法,又失去了原本要降低代码分散的初衷。
总结:
  • 在创建对象时,可以使用内联的handler Block将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler Block来实现,则可直接将Block与相关对象放在一起;
  • 设计API时,如果遇到handler Block,那么可以新增一个参数,使调用者可以通过该参数来决定应该把Block安排在哪个队列上执行。

2.3 使用Block中发生的循环引用

如下代码:
某个类作了如下的调用:
分析下场景:
HOClass的实例对象实例变量_networkFetcher引用获取器,_networkFetcher持有completionHandler,completionHandler又引用_fetchData,相当于持有HOClass的实例对象,所以就造成了循环引用。
解除循环引用的方式很简单,打破这个三角循环,要么是使得_networkFetcher不再引用,要么获取器不再持有completionHandler。
下面是一种解决方式:
另外一种情况是:completion handler所引用的对象最终又引用了这个Block本身。其中获取器持有completion handler,而completion handler中又对获取器的url进行引用。
上面这种保留环,打破也很简单:
  • 如若Block所捕获的对象直接或间接地保留了Block本身,那么就得担心保留环问题;
  • 一定要找个恰当的时机解除保留环,而不能把责任推给API的调用者。

2.4 多用派发队列,少用同步锁

在Objective-C中,多线程执行同一份代码,使用锁来实现某种同步机制,在GCD之前,有两种办法:
其一是“同步Block”:
另外一种就是:
上面两种方法有其缺陷:极端情况下,都会导致死锁,其效率也不高。
替代方案就是:GCD。
上面将保证所有读写的操作都在同一队列中,这相比上面加锁机制,更为高效(GCD基于底层的优化),也更为整洁(所有的同步在GCD中实现)。
上面可以优化的就是,可以将取值方法,异步读取,串行队列里派发异步操作,会开启一个新线程来执行异步操作,而不是同步操作那样所有的操作在同一个线程。如下:
但虽然是优化,不过有个优化陷进,就是执行异步派发是,需要拷贝Block。若拷贝Block的执行时间比Block执行所用的时间长,那么就是个“伪优化”,则比原来更慢。由于本例简单,所以改完之后可能更慢。
多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行。可以采用栅栏函数,这次改用并发队列:
下面是执行:
notion image
并发队列如果发现接下来要处理的块是个栅栏块,那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块,待栅栏块执行过后,再按正常方式继续向下处理。
测试一下性能,这种做法比刚才的肯定更快。
注意,设置函数也可以用同步的栅栏块来实现,那样做可能会更高效,因为异步需要拷贝代码块。
要选方案,还是最好测一下实际的性能。
  • 派发队列可用来表述同步语义,这种做法要比使用@synchronizedNSLock对象更简单;
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做不会阻塞执行异步派发的线程;
  • 使用同步队列及栅栏块,可以令同步行为更高效;

2.5 多用GCD,少用performSelector系列方法

NSObject中可以调用任何方法,最简单如下:
如果选择子在运行期决定,就能体现出此方式的强大之处了。这就相当于在动态绑定上再次使用动态绑定:
使用此特性的代价是:如果在ARC下编译此代码 ,那么编译器会发出下面警告:
warning:performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]
因为无法确定选择子,也就没有运用内存管理规则判断返回值是不是需要释放。ARC采用了比较谨慎的方法,就是不添加释放操作。然而这么做可能导致内存泄漏。下面是一个实例:
这段代码,在执行第一个和第二个选择子时,需要释放ret对象,而第三个则不需要。但是这个问题很容易被忽视,或者用静态分析器也无法侦测到。
编者按:根据苹果的命名规则,第一个和第二个选择子创建对象时,会拥有对象的所有权,所以需要释放。
其次,performSelector方法只返回id类型,即只能是void或者对象类型,而不能是整形等纯量类型。
另外,还有几个performSelector方法如下:
然而,上面的延时执行都可以用dispatch_after来处理:
  • performSelector系列方法在内存管理易有疏漏,它无法确定将要执行的选择子具体是什么,所以ARC编译器也就无法插入适当的内存管理方法;
  • performSelector系列方法所能处理的选择子太过局限,选择子返回值类型及发送方给方法的参数个数都受到限制;
  • 如果想延迟执行,最好不要用performSelector系列方法,而是应该把任务封装到Block里,调用GCD来实现。

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

使用NSOperationNSOperationQueue
  • 取消某个操作;
  • 指定操作间的依赖关系;
  • 通过键值观察机制监控NSOperationNSOperation对象许多属性都适合通过键值观察机制来监听,比如isCancelledisFinished
  • 指定操作的优先级;

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

更优的实现方式:
dispatch_once可以简化代码并且彻底保证线程安全,此外更高效,它没有使用重量级的同步机制。

2.8 不要使用dispatch_get_current_queue

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

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

dispatch_group能够把任务分组,调用者可以等待这组任务执行完毕,也可以提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。
  • 一系列的任务可归入一个dispatch_group中,开发者可以在这组任务完毕时获得通知;
  • 通过dispatch_group,可以在并发时派发队列同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需要编写大量代码。