iOS笔记

[iOS]关于《Effective OC 2.0:编写高质量iO

2017-06-23  本文已影响168人  德山_

第 23 条:通过委托与数据源协议进行对象间通信

  1. Objective-C 可以使用 “委托模式”(Delegate pattern)的编程设计模式来实现对象间的通信:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其 “委托对象”(delegate)。Objective-C 一般利用 “协议” 机制来实现此模式。

  2. 定义协议:

@protocol EOCNetworkingFetcherDelegate
@optional

@interface EOCNetworkingFetcher : NSObject
@property (nonatomic,weak) id<EOCNetworkingFetcherDelegate> delegate;
@end

委托协议名通常时在相关的类名加上Delegate 一词,也是采用 “驼峰法” 来命名。

类可以用一个属性存放其委托对象,属性要用weak 来修饰,避免产生 “保留环”(retain cycle)。

某类若要遵从某委托协议,可以在其接口中声明,也可以在"class-continuation 分类" 中声明,如果要向外界公布此类实现了某协议,就在接口中声明,如果这个协议是个委托协议,通常只会在这个类的内部使用,这样子就在分类中声明就好了。

  1. 如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法,判断这个委托对象能否响应相关的选择子。

NSData *data;
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}

在调用delegate 对象中的方法时,总应该把发起委托的实例也一并传入方法中,这样子,delegate 对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。

  1. delegate 里的方法也可以用于从委托对象中获取信息(数据源模式)。

  2. 在实现委托模式和数据源模式的时,协议中的方法是可选的,我们就会写出大量这种判断代码:

if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}

每次调用方法都会判断一次,其实除了第一次检测的结构有用,后续的检测很有可能都是多余的,因为委托对象本身没变,不太可能会一下子不响应,一下子响应的,所以我们这里可以把这个委托对象能否响应某个协议方法记录下来,以优化程序效率。

将方法响应能力缓存起来的最佳途径是使用 “位段”(bitfield)数据类型。我们可以把结构体中某个字段所占用的二进制位个数设为特定的值。

位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。

struct data {

unsigned int filedA : 8;

unsigned int filedB : 4;

unsigned int filedC : 2;

unsigned int filedD : 1;

}

filedA 位段占用8个二进制位,filedB 位段占用4个二进制位,filedC 位段占用2个二进制位,filedD位段占用1个二进制位。filedA 就可以表示0至255之间的值,而filedD 则可以表示0或1这两个值。

我们可以像filedD 这样子,创建大小只有1的位段,这样子就可以把Boolean 值塞入这一小块数据里面,这里很适合这样子做。

利用位段就可以清楚的表示delegate 对象是否能响应协议中的方法。

@interface EOCNetworkingFetcher ()
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags
@end

//使用
//set flag
_delageteFlags.didReceiveData = 1;

//check flag
if(_delageteFlags.didReceiveData){
//YES
}

可以在delegate 属性的设置方法里面写实现缓存功能所用的代码。

这样子,每次调用delegate 的相关方法之前,就不用检测委托对象是否能响应给定的选择子了,而是直接查询结构体里面的标志。

在相关方法需要调用很多次时,就要思考是否有必要进行优化,分析代码性能,找出瓶颈,使用这个位段这个技术可以提供执行速度。

委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。

将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。

当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情境下,该模式亦称 “数据源协议”(data source protocal)。

若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。

第 24 条:将类的实现代码分散到便于管理的数个分类之中

一个类经常有很多方法,尽管代码写的比较规范,这个文件还是会越来越大,定位问题以及阅读上都会造成不便。我们可以通过 “分类” 机制来把代码按逻辑划分到几个分区中。

通过分类机制,可以把类代码分成很多个易于管理的小块,以便单独检视。

可以考虑创建Private 分类,将一些不是公共API 的方法,隐藏起来。写程序库的时候,加上不暴露头文件,使用者就不知道库里还有这些私有方法。

使用分类机制把类的实现代码划分成易于管理的小块。

将应该视为 “私有” 的方法归入为叫Private 的分类中,以隐藏实现细节。

第 25 条:总是为第三方类的分类名称加前缀

分类机制常用于向无源码的既有类中新增新功能,但是在使用的时候要十分小心,不然很容易产生Bug。因为这个机制时在运行期系统加载分类时,将其方法直接加到原类中,这里要注意方法重名的问题,不然会覆盖原类中的同名方法。

一般用前缀来区分各个分类的名称与其中所定义的方法。

不要轻易去利用分类来覆盖方法,这里需要慎重考虑。

向第三方类中添加分类时,总应该给其名称加上你专用的前缀。

向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀

第 26 条:勿在分类中声明属性

可以利用运行期的关联对象机制,为分类声明属性,但是这种做法要尽量避免,因为除了 "class-continuation 分类" 之外,其他分类都无法向类中新增实例变量,因此,他们无法把实现属性所需的实例变量合成出来。

在分类定义属性的时候,会报警告,表明此分类无法合成该属性相关的实例变量,所以开发者需要在分类中为该属性实现存取方法。

利用关联对象机制可以解决分类中不能合成实例变量的问题。自己实现存取方法,但是要注意该属性的内存管理语义(属性特质)。

@property (nonatomic,copy) NSString *name;

static const void *kViewControllerName = &kViewControllerName;

  1. 在可以修改源代码的情况下,尽量把属性定义在主接口中,这里是唯一能够定义实例变量的地方,属性只是定义实例变量及相关存取方法所用的 “语法糖”。

  2. 由于实现属性所需的全部方法都已实现,所以不会再为该属性自动合成实例变量了。

尽量把封装数据所用的全部属性都定义在主接口里。

在 “class-continuation 分类” 之外的其他分类中,可以定义存取方法,但尽量不要定义属性。

第 27 条:使用 ”class-continuation 分类“ 隐藏实现细节

  1. ”class-continuation 分类“ 必须定义在本身类的实现文件中,而且这里是唯一可以声明实例变量的分类,而且此分类没有特定的实现文件,这个分类也没有名字。这里可以定义实例变量的原因是 “ 稳固的ABI” 机制,我们无须知道对象的大小就可以直接使用它。

@interface EOCPerson ()

@end

  1. 可以将不需要要暴露给外界知道的实例变量及方法写在 “class-continuation 分类” 中。

  2. 编写Objective-C++ 代码时候,使用 “class-continuation 分类” 会十分方便。因为对于引用了C++的文件的实现文件需要用.mm 为扩展名,表示编译器应该将此文件按照Objective-C++ 来编译。C++ 类必须完全引入,编译器要完整地解析其定义才能得知这个C++ 对象的实例变量大小。如果把对C++ 类的引用写在头文件的话,其他引用到这个类也会引用到这个C++ 类,就也需要编译成Objective-C++ 才行,这样子很容易失控。

这里可以利用 “class-continuation 分类” 把引用C++ 类的细节写到实现文件中,这样子别的类引用这个类就不会受到影响,甚至都不知道这个类底层实现混有C++ 代码。

  1. 使用 “class-continuation 分类” 还可以将头文件声明 “只读” 的属性扩展成 “可读写”,以便在类的内部可以设置其值。

  2. 我们通常不直接访问实例变量,而是通过设置方法来做,因为这样子可以触发 “键值观测” (Key-Value Observing,KVO)通知。

  3. 若对象所遵循的协议只应视为私有,也可以同过“class-continuation 分类” 来隐藏。

通过 “class-continuation 分类” 向类中新增实例变量。

如果某属性在主接口中声明为 “只读”,而类的内部又要用设置方法修改此属性,那么就在 “class-continuation 分类” 中将其扩展为 “可读写”。

把私有方法的原型声明在 “class-contiunation 分类” 里面。

若想使类所遵循的协议不为人所知,则可于 “class-contiunation 分类” 中声明。

第 28 条:通过协议提供匿名对象

@property (nonatomic,weak) id<EOCDelegate> delegate;

该属性类型是id<EOCDelegate> 的,所以实际上任何类的都能充当这一属性,即便该类不继承NSObject 也可以,只要遵循EOCDelegae 协议就可以了,对于具备此属性的类来说,delegate 就是 “匿名的”。

协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id 类型,协议里规定了对象所应实现的方法。

使用匿名对象来隐藏类型名称(或类名)。

如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。


内存管理

第 29 条:理解引用计数

引用计数工作原理

Objective-C 语言使用引用计数来管理内存,每个对象都有个可以递增递减的计数器,用以表示当前有多少个事物想令此对象继续存活下去。

NSObject 协议声明下面三个方法用于操作计数器,以递增或递减其值:

retain 递增保留计数

release 递减保留计数

autorelease 待稍后清理 “自动释放池” 时,再递减保留计数

  1. 在调用release 之后,对象所占的内存可能会被回收,这样子在调用对象的方法就可能使程序崩溃,这里 “可能” 的意思是对象所占的内存在 “解除分配” (deallocated)之后,只是放回 “可用内存池”(avaiable pool)。若果执行方法时尚未覆写对象,那么对象仍然有效。

  2. 为避免在不经意间使用无效对象,一般在调用完release 之后都会清空指针,保证不会出现可能指向无效对象的指针,这种指针通常被称为 “悬挂指针”(dangling pointer)。

自动释放池

调用release 会立刻递减对象的保留计数(这里可能会令系统回收此对象),调用autorelease 方法,是在稍后递减计数,通常是在下一次 “事件循环” 时递减。

此特性很有用,尤其是在返回对象时更应该用它

这里返回的str 对象的保留计数会比期望值多1,因为调用alloc 会令保留计数+1,这里又没有对应的释放操作,这样子就意味着调用者要负责处理这多出来的保留操作。在这个方法又不能释放str,否则还没等方法返回,str 这个对象就被释放了。这里应该用autorelease ,它会在稍后释放对象,保证这里可以保证调用者可以先用这个str 对象。

  1. autorelease 能延长对象声明周期,使其在跨越方法调用边界后依然可以存活一段时间。

保留环

呈环状相互引用的多个对象,相互持有,这将导致内存泄漏,这里循环中的对象其保留计数不会降为0。

通常采用 “弱引用” 来解决此问题,或者从外界命令某个对象不再保留另外一个对象来打破保留环,从而避免内存泄漏。

引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。

在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

第 30 条:以 ARC 简化引用计数

内存泄漏:没有正确的释放已经不再使用的内存。

自用引用计数预先加入适当的保留或释放操作来避免内存泄漏,使用ARC 时,引用计数实际上还是要执行的,只是保留与释放操作是由ARC 自动添加的。

ARC 会自动执行retain、release、autorelease、dealloc等操作,所以在ARC 下调用这些内存管理方法是非法的。因为ARC 会分析何处应该自动调用内存管理方法,所以我们再手动调用的话,会干扰其工作。

实际上,ARC 在调用这些方法时,并不是普通的Objective-C 消息派发机制,而是直接调用其底层的C 语言函数,这样子性能会更好。

使用ARC 时必须遵循的方法命名规则

  1. 将内存管理语义在方法名中表示出来,若方法名以下列词语开头,则返回的对象归
    调用者所有:

alloc

new

copy

mutableCopy

  1. 将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化。
    变量的内存管理语义

ARC 也会处理局部变量与实例变量的内存管理。
我们通常会给局部变量加上修饰符来打破 “块”(block)所引入的 “保留环”(retain cycle)。

ARC 如何清理实例变量

对实例变量进行内存管理,必须在 “回收分配给对象的内存” 时生成必要的清理代码。凡事具备强引用的变量,都必须释放,ARC 会在dealloc 方法中插入这些代码。

ARC 会借用Objective-C++ 的一项特性来生成清理代码,在回收对象时,待回收对象会调用所有C++ 对象的析构函数,编译器如果发现某个对象里含有C++ 对象,就会生成名为.cxx_desteuct 的方法,ARC 借助此特性,在该方法中生成清理内存所需的代码。

对于非Objective-C 的对象,然后需要我们手动清理。CFRelease();

覆写内存管理方法

非ARC 时可以覆写内存管理方法,在ARC 下禁止覆写内存管理方法,会干扰到ARC 分析对象生命周期的工作。

有ARC 之后,程序员就无需担心内存管理问题了。使用ARC 来编程,可省去类中的许多 “样板代码”。

ARC 管理对象生命周期的办法基本上是:在适合的地方插入 “保留” 及 “释放” 操作。在ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行 “保留”及 “释放” 操作。

ARC 只负责管理Objective-C 对象的内存。尤其要注意:CoreFoundation 对象不归ARC 管理,开发者必须适时调用CFRetain/CFRelease。

第 31 条:在 dealloc 方法中只释放引用并解除监听

对象在经历生命周期后,最终会为系统回收,这时候就要执行dealloc 方法。每个对象生命周期内,此方法只会调用一次,也就是保留计数为0 的时候,绝对不能自己调用dealloc 方法,运行期会在适当的时候调用,一旦调用,对象就不再有效了,后续的方法调用均是无效的。

dealloc 方法主要是释放对象所拥有的引用,也就是把Objective-C 对象都释放掉,ARC 会通过自动生成的.cxx_desteuct 方法,在dealloc 中为你自动添加这些释放代码。但是其他非Objective-C 对象就需要自己手动释放了。

dealloc 方法通常还需要把原来配置过的观测行为都清理掉,例如通知等。

对于开销较大或者系统内稀缺的资源不应该等到dealloc 才清理(文件描述符、套接字、大块内存等),因为dealloc 并不会在特定的时机调用,因为有可能还有别的对象持有它。应该自己实现一个方法,当应用程序用完资源对象后,就调用此方法,这样子对象的生命周期就更加明确了。

调用dealloc 方法的那个线程会执行 “最终的释放操作”,令对象保留计数为0,而某些方法必须在特定的线程调用,若在dealloc 中调用那么方法,无法保证当前的线程就是那个方法所需的线程。在dealloc 里尽量不要去调用方法,包括属性的存取方法,因为在这些方法可能会被覆写,并在其中做一些无法在回收阶段安全执行的操作。

在dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的 “键值观测”(KVO)或NSNotification 等通知,不要做其他事情。

如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close 方法。

执行异步任务的方法不应在dealloc 里调用;只有在正常状态下执行的那些方法也不应在dealloc 里调用,因为此时对象已处于回收的状态。

第 32 条:编写 “异常安全代码” 时留意内存管理问题

纯C 中没有异常,C++与Objective-C 都支持异常,在运行期系统中C++与Objective-C 异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序” 来捕获。

Objective-C 错误模型表明,异常只应发生严重错误后抛出,发生异常如何管理内存很重要,在try 块中保留某个对象的,但是在释放它之前抛出异常了,这时候就无法正常释放了,这时候需要借助@finally 块来保证释放对象的代码一定会执行,且只执行一次。

在ARC 不会自动生成处理异常中的代码,因为这样子需要加入大量的样板代码,以便追踪待清理的对象,从而在抛出异常时将其释放。可以这段代码会严重运行期的性能,还会增加应用程序的大小。

可以通过-fobjc-arc-exceptions 这个编译编织来开启这个功能,但是这个功能不应该作为生成这种安全处理异常所用的附加代码,应该是让代码处于Objective-C++模式。

捕获异常时,一定要注意将try 块内创建的对象清理干净。

在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

第 33 条:以弱引用避免保留环

几个对象都已某种方式互相引用,从而形成 “环”,这种情况通常会泄漏内存,因为没有东西引用环中对象,这样子环里的对象互相引用,不会被系统回收。

避免保留环的最佳方式就是弱引用,来表示 “非拥有关系”,unsafe_unretained、weak 修饰都是可以达到的。unsafe_unretained 表示属性值可能不安全,有可能系统把属性所指的对象回收了,但是这个属性依然指向那块地址,那么再调用它的方法可能会使程序崩溃,用weak 修饰的时候,在所指对象被回收的时候,会将属性的指针置为nil。

一般来说,如果不拥有某对象,就不要保留它,这条规则对collection 例外,collection 虽然不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。

将某些引用设为weak,可避免出现 “保留环”。

weak 引用可以自动清空,也可以不自动清空。自动清空是随着ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

第 34 条:以 “自动释放池块” 降低内存峰值

  1. 释放对象有两种方式:

一种是调用release 方法,使其保留计数立即递减

一种是调用autorelease 方法,将对象放入 “自动释放池” 中,自动释放池用于存放那些需要稍后某个时刻释放的对象,清空(drain)自动释放池时,系统会向其中的对象发送release 消息。

  1. 创建自动释放池,系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行 “事件循环”时,都会将其清空。自动释放池于左边花括号创建,并于对应的右花括号自动清空。位于自动释放池范围内的对象,会在末尾处受到release 消息。

@autoreleasepool {
//...
}

  1. 内存峰值:是指应用程序在某个特定时段内的最大内存用量。

  2. 对象有可能会放在自动释放池里面,需要等到线程执行下一次事件循环才会清空,这里会导致应用程序所占内存会持续增加,等到临时对象释放的时候,内存用量又会突然下降。我们现在就想把这个内存峰值给降低下来。

  3. 可以增加一个自动释放池来解决这个问题:这样子对象就会加入到这个释放池,而不是线程的主池中,每次循环都创建和释放这个释放池。

for (int i = 0;i < 100000;i++){
@autorelease{
NSObject *object = [NSObject new];
}
}

  1. 自动释放池机制就像 “栈” 一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池。

  2. 对于是否需要用池来优化效率,这个得考虑清楚来,因为自动释放池的创建还是有一丢丢开销的,所以尽量不要建立额外的自动释放池。

自动释放池排布在栈中,对象收到autorelease 消息后,系统将其放入到最顶端的池里。

合理运用自动释放池,可降低应用程序的内存峰值。

@autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。


第 35 条:用 “僵尸对象” 调试内存管理问题

向已回收的对象发送消息是不安全的,是否崩溃这个是看对象所占的内存有没有为其他内容所覆写。

Cocoa 提供 “僵尸对象”(Zombie Object)这个非常方便的功能,开启后,运行期系统会把已经回收的实例转换成特殊的 “僵尸对象”,而不会真正回收它们。这个对象所在的核心内无法重用,因此不可能遭到覆写,僵尸对象收到消息后,会抛出异常。

使用:Xcode Scheme 中的Enable Zombie Objects 选项,打开会将NSZombieEnabled 环境变量设成YES。

系统在即将回收时,会执行一个附加步骤,将对象转换成僵尸对象,而不彻底回收。僵尸类是从名为NSZombie 的模版类复制出来的。NSZombie 类并未实现任何方法,此类没有超类,因此跟NSObject 一样,也是一个 "根类",该类只有一个实例变量,叫做isa,所以发给他的消息都要经过 “完整的消息转发机制” 。

在完整的消息转发机制中,forwarding 是核心,检查接受消息的对象所属的类名,若是NSZombie ,则表示消息接受者是僵尸对象,需要特殊处理。

系统在回收对象时,可以不将其真的回收,而是把它转化成僵尸对象。通过环境变量NSZombieEnabled 可开启此功能。

系统会修改对象的isa 指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

第 36 条:不要使用 retainCount

每个对象都有一个计数器,其表明还有多少个其他对象想令此对象继续存活。在ARC retainCount 这个方法已经废弃了,但是在非ARC 中也不应该调用这个方法,因为这个保留计数只是返回某个时间点的值,并不会联系上下文给出真正有用的值。

retainCount 可能永远不返回0,因为系统有时候会优化对象的释放行为,在保留计数为1的时候就把它回收了。

不应该依靠保留计数的具体址来编码。

对象的保留计数看似有用,实则不然,因为任何给定时间点上的 “绝对保留计数”(absolute retain count)都无法反映对象生命期的全貌。

引入ARC 之后,retainCount 方式就正式废止了,在ARC 下调用方法会导致编译器报错。


块与大中枢派发

第 37 条:理解 “块” 这一概念

块可以实现闭包。

块的基础知识

  1. 块用 “^” 符号来表示,后面跟着一对花括号,括号里面是块的实现代码。块其实就是个值,而且自有其相关类型,可以赋值给变量;块类型的语法和函数指针类似。

^{
//block implementation herer
}

//这里定义了名为someBlock 的变量
//块类型的语法结构如下
//return_type (^block_name)(parameters)
void (^someBlock)() = ^{
//block implementation herer
}

  1. 在声明块的范围内,所有变量都可以被其捕获。默认情况下被块捕获的变量是不可以在块里修改的,不过可以在声明变量的时候加上__block 修饰符,这样子就可以在块内修改了。

  2. 如果块所捕获的变量是对象类型,那么就会自动保留它,在系统释放这个块的时候,也会将其一并释放。

  3. 块总能修改实例变量,所以在声明时无须加__block。不过如果通过读取或写入操作捕获了实例变量,那么也会自动把self 变量一并捕获了,因为实例变量是与self 所指代的实例关联在一起的。

块的内部结构

  1. 块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class 对象的指针(isa 指针)。
  1. invoke 变量是这个函数指针,指向块的实现代码。函数原型至少要接受一个void* 型的参数,此参数代表块。为什么要把块对象作为参数传进来呢,因为在执行块的时候,要从内存中把这些捕获到的变量读出来。

descriptor 变量是指向结构体的指针,这个结构体包含块的一些信息。

全局块、栈块及堆块

  1. 定义块的时候,其所占的内存区域是分配在栈中,意思就是,块只在定义它的那个范围内有效。

void (^block)();
if(***){
block = ^(){
NSLog(@"Block A");
};
}else{
block = ^(){
NSLog(@"Block B");
};
}
block();

/*定义在if else 语句中的两个块都分配在栈内存中,编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块内存覆写掉。所以这里执行block() 有危险。

为了解决这个问题,可以给块发送copy 消息以拷贝之。这样子的话,就可以把块从栈复制到堆可。一旦复制到堆上,块就成了带引用计数的对象了,后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。
*/

  1. 全局块声明在全局内存里,而且也不能被系统回收,相当于单例。由于运行该块所需的全部信息在编译期确定,所以可以把它作为全局块,这是一种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

块是C、C++、Objective-C 中的词法闭包。

块可接受参数,也可返回值。

块可以分配在栈和堆上,也可以是全局的。分配在栈上的块可以拷贝到堆里,这样的话,就和标准的Objective-C 对象一样,具备引用计数了。

第 38 条:为常用的块类型创建 typedef

每个块都具备其 “ 固定类型”,因而可将其赋值给适当类型的变量。

由于块类型的语法比较复杂难记,我们可以给块类型起个别名。用C 语言中的 “ 类型定义” 的特性。typedef 关键字用于给类型起个易读的别名。

typedef int(^EOCSomeBlock)(BOOL flag, int value);

EOCSomeBlock block = ^(BOOL flag, int value){
//to do
};

以typedef 重新定义块类型,可令块变量用起来更加简单。

定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型向冲突。

不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应depedef 中的块签名即可,无须改动其他typedef。

第 39 条:用handle 块降低代码分散程度

场景:异步方法执行完任务,需要以某种手段通知相关代码。经常使用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议,对象成了delegate 之后,就可以在相关事件发生时得到通知了。

使用块来写的话,代码会更清晰,使得代码更加紧致。

在创建对象时,可以使用内联的handle 块将相关业务逻辑一并声明。

在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改用handle 块来实现,则可直接将块与相关对象放在一起。

设计API 时如果用到handle 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

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

如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。

一定要找个合适的时机解除保留环,而不能把责任推给API 的调用者。

第 41 条:多用派发队列,少用同步锁

  1. 如果有多个线程要执行同一份代码,那么有时可能会出问题,这种情况下,通常要使用锁来实现某种同步机制。在GCD 出现之前,有两种办法:

采用内置的 “同步块”(synchronization block)

/*

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕,执行到代码结尾,锁就释放了。

但是,滥用 @synchronized(self) 则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。
*/

直接使用NSLock 对象,也可以使用NSRecursiveLock “递归锁”,线程能多次持有该锁,而且不会出现死锁。

_lock = [[NSLock alloc] init];

  1. 对于上面两种方法,有些缺陷,同步块会导致死锁,直接使用锁对象,遇到死锁,就会非常麻烦。

  2. GCD 以更简单、更高效的形式为代码加锁。

例子:属性是开发者经常需要同步的地方,可以使用atomic 特质来修饰属性,来保证其原子性,每次肯定可以从中获取到有效值,然而在同一个线程上多次调用获取方法(getter),每次获取到结果未必相同,在两次访问操作之间,其他线程可能会写入新的属性值。

使用 “串行同步队列”,将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。

_syncQueue = dispatch_queue_create("com.pengxuyuan.syncQueue", NULL);

/*
上面是用串行同步队列来保证数据同步:把设置操作与获取操作都安排在序列化的队列里执行,这样子,所有针对属性的访问操作都是同步的了。
*/

/*
进一步优化,设置方法不一定非得是同步的,因为不需要返回值。这样子可以提高设置方法的执行速度,而读取操作与写入操作依然会按照顺序执行。

但是这里可能发现这种写法比原来慢,因为执行异步派发时,需要拷贝块。
*/

/*
我们现在目的就是要做到:多个获取方法可以并发执行,而获取方法与设置方法不能并发执行。

我们还可以使用并发队列来实现,现在都是在并发队列上面执行任务,但是顺序不能控制,我们可以用栅栏(barrier)来解决。

这两个函数可以向队列派发块,将其作为栅栏来使用:
dispatch_barrier_sync(dispatch_queue_t queue,^(void)block)
dispatch_barrier_async(dispatch_queue_t queue,^(void)block)

在队列中,栅栏块必须单独执行,不能与其他块并行,这只对并发队列有意义,因为串行队列中的块总是按照顺序逐个执行的。并发队列如果发现接下来要处理的块是栅栏块,那么就一直要等到当前所有的并发块都执行完毕,才会单独执行这个栅栏块。执行完栅栏块,再按照正常方式向下处理。
*/

-----> 现在并发队列 还不能满足要求
_syncQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

-----> 转换写法 用栅栏块控制属性的设置方法 不能并行
_syncQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized 块或则NSLock 对象更简单。

将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。

使用同步队列及栅栏块,可以使同步行为更加高效。

第 42 条:多用GCD,少用performSelector 方法

performSelector 可以任意调用方法,还可以延迟调用,还可以指定运行方法所用的线程。

但是如果是动态来调用performSelector 方法的时候,编译器都不知道执行的选择子是什么,必须到了运行期才能确定,这种情况在ARC 下会报警告,因为编译器不知道方法名,所以不能运用ARC 内存管理规则来判定返回值是否应该释放,对于这种情况ARC 不会帮我们添加任何释放操作。

performSelector 方法调用的时候对于返回类型只能是void或对象类型,对于有返回值的需要自己做多次转换,对于参数的也最多只能传2个,介于此performSelector 还是比较不方便的。

对于performSelector 遇到的问题,我们都可以用GCD 解决。

performSeletor 系列方法在内存管理方面容易有疏忽。它无法确定将要执行的选择子具体是什么,因而ARC 编译器也就无法插入适当的内存管理方法。

performSeletor 系列方法所能处理的选择子太过局限了,选择子的返回类型及发送給方法的参数个数收到限制。

如果想把任务放在另一个线程上执行,那么最好不要用performSeletor 系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。

第 43 条:掌握GCD 及操作队列的使用时机

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

GCD 是纯C 的API,操作队列的则是Objective-C 的对象。用NSOperationQueue 类的“addOperationWithBlock” 方法搭配NSBlockOperation 类操作队列,其语法与纯GCD 方式非常类似。使用NSOperation 及NSOperationQueue 的好处如下:

取消某个操作。如果使用操作队列,那么想取消操作是很容易的。运行任务之前,可以在NSOperation 对象调用cancel 方法,该方法会设置对象内的标识位,用以表明此任务不需执行,不过,已经启动的任务无法取消。若不是操作队列,而是把块安排到GCD 队列,那就无法取消了。那套架构是 “安排好任务之后就不管了”。开发者可以在应用层自己来实现取消功能,不过这样子做需要编写很多代码,而那些代码其实已经由操作队列实现好了。

指定操作间的依赖关系。一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖关系,使特定的操作必须在另外一个操作顺序执行完毕方可执行,比方说,从服务器下载并处理文件的动作,可以用操作来表示,而在处理其他文件之前,必须先下载 “清单文件”。后续的下载操作,都要依赖于先下载清单文件这一操作。如果操作队列允许并发的话,那么后续的多个下载操作就可以同时执行,但前提是它们所依赖的那个清单文件下载操作已经执行完毕。

通过键值观测机制监控NSOperation 对象的属性。NSOperation 对象有许多属性都适合通过键值观测机制(KVO)来监听,比如可以通过isCancalled 属性来判断任务是否取消。如果想在某个任务变更期状态时得到通知,或是想用比GCD 更为精细的方式来控制所要执行的任务,那么键值观测机制会很有用。

制定操作的优先级。操作的优先级表示此操作与队列其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。操作队列的调度算法已经比较成熟。反之,GCD 则没有直接实现此功能的办法,GCD 的队列有优先级,但是是针对整个队列来说的,而不是针对每个块来说的。对于优先级这一点,操作队列所提供的功能比GCD 更为便利。

重用NSOperation 对象。系统内置类一些NSOperation 的子类供开发者调用,要是不想用这些固有子类的话,那就得自己来创建了。这些类就是普通的Objective-C 对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。这比派发队列中哪些简单的块要强大。这些NSOperation 类可以在代码中多次使用。

在解决多线程与任务管理问题时,派发队列并非唯一方案。

操作队列提供了一套高层的Objective-C API,能实现纯GCD 所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD 来实现,则需另外编写代码。

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

一系列任务可归入一个dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。

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

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

  1. 对于单例我们创建唯一实例,之前都是用@synchronized 加锁来解决多线程的问题,GCD 提供了一个更加简单的方法来实现。

//单例
+(instancetype)shareInstance{
static dispatch_once_t onceToken;
static PXYAdvertisingPagesHelper *shareInstance;
dispatch_once(&onceToken, ^{
shareInstance = [PXYAdvertisingPagesHelper new];
shareInstance.adTimeout = 5.0;
});
return shareInstance;
}

  1. 使用dispatch_once 可以简化代码,并且彻底保证线程安全。

经常需要编写 “只需执行一次的线程安全代码”。通常使用GCD 所提供的dispatch_once 函数,很容易就能实现此功能。

标记应该声明在static 或 global 作用域中,这样的话,在把只需执行一次的快传给dispatch_once 函数时,传进去的标记也是相同的。

第 46 条:不要使用dispatch_get_current_queue

Mac OS X 与 iOS 的UI 事务都需要在主线程上执行,而这个线程就相当于GCD 中的主队列。

dispatch_get_current_queue 这个函数返回当前正在执行代码的队列,但是在iOS 6.0版本起,已经弃用这个函数了。

该函数有种典型的错误用法,就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇到的死锁问题。

看下下面这个代码:

dispatch_queue_t queueA = dispatch_queue_creat("com.pengxuyuan.queueA",NULL);
dispatch_queue_t queueB = dispatch_queue_creat("com.pengxuyuan.queueB",NULL);

dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dipatch_sync(queueA, ^{
//DeadLock
});
});
});

//这里是个典型的死锁现象,queueA 串行队列上面的同步任务相互等待了。

dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^();
if(dispatch_get_current_queue() == queueA){
block();
}else{
dipatch_sync(queueA, block);
}
});
});
//但是用dispatch_get_current_queue 这个来判断,当前返回的是queueB,这里还是会去执行dipatch_sync(queueA, block); 造成死锁

  1. 因为队列有层级关系,所以 “检查当前队列是否为执行同步派发所用的队列” 这种办法,并不是总是奏效的。

  2. 要解决这个问题,可以通过GCD 所提供的功能来设定 “队列特有数据”,此功能可以把任意数据以键值对的形式关联到队列里。假如根据指定的键获取不到关联数据,那么就会沿着层级体系向上查找,直到找到数据或到根队列为止。

dispatch_queue_t queueA = dispatch_queue_creat("com.pengxuyuan.queueA",NULL);
dispatch_queue_t queueB = dispatch_queue_creat("com.pengxuyuan.queueB",NULL);

static int kQueueSpecific;
CFStringRef queueSepcificValue = CFSTR("queueA");

dispatch_queue_set_specific(queueA,
&kQueueSpecific,
(void *)queueSepcificValue,
(dispatch_function_t));
dispatch_sync(queueB, ^{
dispatch_block_t block = ^();
CFStringRef retrievedValue = dispatch_queue_set_specific(&kQueueSpecific);
if(retrievedValue){
block();
}else{
dipatch_sync(queueA, block);
}
});

dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。

由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述 “当前队列” 这一概念。

dispatch_get_current_queue 函数用于解决由不可重入的代码引发的死锁,然而能用此函数的解决的问题,通常也能改用 “队列特定数据” 来解决。

上一篇下一篇

猜你喜欢

热点阅读