专栏名称: Cocoa开发者社区
CocoaChina苹果开发中文社区官方微信,提供教程资源、app推广营销、招聘、外包及培训信息、各类沙龙交流活动以及更多开发者服务。
目录
相关文章推荐
iOS中文站  ·  iPhone ... ·  2 天前  
iOS中文站  ·  iPhone ... ·  2 天前  
51好读  ›  专栏  ›  Cocoa开发者社区

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

Cocoa开发者社区  · 公众号  · ios  · 2017-06-23 11:17

正文

原文地址关于Effective Objective-C 这本书一些有趣的东西


协议与分类


第 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 delegate;

@end


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

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

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


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


NSData *data;

if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){

    [_delegate networkFetcher:self didRecevieData:data];

}


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


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


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


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 条:将类的实现代码分散到便于管理的数个分类之中


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

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

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


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

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


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


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

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

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


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

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


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


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


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


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


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

}


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


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


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

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


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


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


@interface EOCPerson ()


@end


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


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


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


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


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


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


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

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

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

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


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


@property (nonatomic,weak) id delegate;


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


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

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

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


内存管理


第 29 条:理解引用计数


引用计数工作原理


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

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


  • retain 递增保留计数

  • release 递减保留计数

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


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


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


自动释放池


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


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


- (NSString *)stringValue {

    NSString *str = [[NSString alloc] 

                initWithFormat:@"I am this %@",self];

    return str;

}


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


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


保留环


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

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


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

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


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


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

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

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

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


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


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

调用者所有:


  • alloc

  • new

  • copy

  • mutableCopy


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

变量的内存管理语义


ARC 也会处理局部变量与实例变量的内存管理。

我们通常会给局部变量加上修饰符来打破 “块”(block)所引入的 “保留环”(retain cycle)。


ARC 如何清理实例变量


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

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

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


覆写内存管理方法


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


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

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

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


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


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

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

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

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

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


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

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

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


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


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

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

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

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


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

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


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


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

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

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


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

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


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


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


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

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


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


@autoreleasepool {

//...

}


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


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


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


for (int i = 0;i < 100000;i++){

    @autorelease{

        NSObject *object = [NSObject new];

    }

}


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


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


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

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

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


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


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

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

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

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

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


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

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


第 36 条:不要使用 retainCount


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

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

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


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

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


块与大中枢派发


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


块可以实现闭包。


块的基础知识


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


^{

    //block implementation herer    

}


//这里定义了名为someBlock 的变量

//块类型的语法结构如下

//return_type (^block_name)(parameters)

void (^someBlock)() = ^{

    //block implementation herer    

}


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


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


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


块的内部结构


1. 块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class 对象的指针(isa 指针)。



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


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


全局块、栈块及堆块


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


void (^block)();

if(***){

    block = ^(){

        NSLog(@"Block A");

    };

}else{

    block = ^(){

        NSLog(@"Block B");

    };

}

block();


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


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

*/


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


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

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

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


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


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


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


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


EOCSomeBlock block = ^(BOOL flag, int value){

    //to do

};


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

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

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


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


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

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


  • 在创建对象时,可以使用内联的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];

}


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


3. 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 方法


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

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

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

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


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

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

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


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


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

  2. 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


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


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


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


  4. 看下下面这个代码:


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); 造成死锁


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


6. 要解决这个问题,可以通过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 函数用于解决由不可重入的代码引发的死锁,然而能用此函数的解决的问题,通常也能改用 “队列特定数据” 来解决。


剩下部分实在放不下了(47-52条):


Effective Objective-C 2.0 总结(七)