[iOS]关于《Effective OC 2.0:编写高质量iOS与OS X代码的52个有效方法》这本书一些有趣的东西(下)

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

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

  2. 定义协议:

@protocol EOCNetworkingFetcherDelegate
@optional

  • (void)newworkingFetcher:(EOCNetworkingFetcher *)fetcher
    didRecevieData:(NSData *)data;
  • (void)newworkingFetcher:(EOCNetworkingFetcher *)fetcher
    didFailWithError:(NSError *)error;
    @end

@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;

  • (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, kViewControllerName, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }

  • (NSString *)name {
    NSString *myName = objc_getAssociatedObject(self, kViewControllerName);
    return myName;
    }

  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 方法,是在稍后递减计数,通常是在下一次 “事件循环” 时递减。

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

  • (NSString *)stringValue {
    NSString *str = [[NSString alloc]
    initWithFormat:@"I am this %@",self];
    return str;
    }

这里返回的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)

  • (void)synchronizedMethod {
    @synchronized(self){
    //safe
    }
    }

/*

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

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

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

_lock = [[NSLock alloc] init];

  • (void)synchronizedMethod {
    [_lock lock];
    //safe
    [_lock unlock];
    }
  1. 对于上面两种方法,有些缺陷,同步块会导致死锁,直接使用锁对象,遇到死锁,就会非常麻烦。

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

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

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

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

  • (NSString *)name {
    __block NSString *tempName;
    dispatch_sync(_syncQueue, ^{
    tempName = _name;
    });
    return tempName;
    }

  • (void)setName:(NSString *)name {
    dispatch_sync(_syncQueue, ^{
    _name = name;
    });
    }

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

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

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

  • (void)setName:(NSString *)name {
    dispatch_async(_syncQueue, ^{
    _name = name;
    });
    }

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

我们还可以使用并发队列来实现,现在都是在并发队列上面执行任务,但是顺序不能控制,我们可以用栅栏(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);

  • (NSString *)name {
    __block NSString *tempName;
    dispatch_sync(_syncQueue1, ^{
    tempName = _name;
    });
    return tempName;
    }

  • (void)setName:(NSString *)name {
    dispatch_async(_syncQueue1, ^{
    _name = name;
    });
    }

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

  • (NSString *)name {
    __block NSString *tempName;
    dispatch_sync(_syncQueue1, ^{
    tempName = _name;
    });
    return tempName;
    }

  • (void)setName:(NSString *)name {
    dispatch_barrier_async(_syncQueue1, ^{
    _name = name;
    });
    }

派发队列可用来表述同步语义(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 函数用于解决由不可重入的代码引发的死锁,然而能用此函数的解决的问题,通常也能改用 “队列特定数据” 来解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容