[iOS]关于《Effective OC 2.0:编写高质量iO
第 23 条:通过委托与数据源协议进行对象间通信
-
Objective-C 可以使用 “委托模式”(Delegate pattern)的编程设计模式来实现对象间的通信:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其 “委托对象”(delegate)。Objective-C 一般利用 “协议” 机制来实现此模式。
-
定义协议:
@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 分类" 中声明,如果要向外界公布此类实现了某协议,就在接口中声明,如果这个协议是个委托协议,通常只会在这个类的内部使用,这样子就在分类中声明就好了。
- 如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法,判断这个委托对象能否响应相关的选择子。
NSData *data;
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}
在调用delegate 对象中的方法时,总应该把发起委托的实例也一并传入方法中,这样子,delegate 对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。
-
delegate 里的方法也可以用于从委托对象中获取信息(数据源模式)。
-
在实现委托模式和数据源模式的时,协议中的方法是可选的,我们就会写出大量这种判断代码:
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;
}
-
在可以修改源代码的情况下,尽量把属性定义在主接口中,这里是唯一能够定义实例变量的地方,属性只是定义实例变量及相关存取方法所用的 “语法糖”。
-
由于实现属性所需的全部方法都已实现,所以不会再为该属性自动合成实例变量了。
尽量把封装数据所用的全部属性都定义在主接口里。
在 “class-continuation 分类” 之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
第 27 条:使用 ”class-continuation 分类“ 隐藏实现细节
- ”class-continuation 分类“ 必须定义在本身类的实现文件中,而且这里是唯一可以声明实例变量的分类,而且此分类没有特定的实现文件,这个分类也没有名字。这里可以定义实例变量的原因是 “ 稳固的ABI” 机制,我们无须知道对象的大小就可以直接使用它。
@interface EOCPerson ()
@end
-
可以将不需要要暴露给外界知道的实例变量及方法写在 “class-continuation 分类” 中。
-
编写Objective-C++ 代码时候,使用 “class-continuation 分类” 会十分方便。因为对于引用了C++的文件的实现文件需要用.mm 为扩展名,表示编译器应该将此文件按照Objective-C++ 来编译。C++ 类必须完全引入,编译器要完整地解析其定义才能得知这个C++ 对象的实例变量大小。如果把对C++ 类的引用写在头文件的话,其他引用到这个类也会引用到这个C++ 类,就也需要编译成Objective-C++ 才行,这样子很容易失控。
这里可以利用 “class-continuation 分类” 把引用C++ 类的细节写到实现文件中,这样子别的类引用这个类就不会受到影响,甚至都不知道这个类底层实现混有C++ 代码。
-
使用 “class-continuation 分类” 还可以将头文件声明 “只读” 的属性扩展成 “可读写”,以便在类的内部可以设置其值。
-
我们通常不直接访问实例变量,而是通过设置方法来做,因为这样子可以触发 “键值观测” (Key-Value Observing,KVO)通知。
-
若对象所遵循的协议只应视为私有,也可以同过“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 待稍后清理 “自动释放池” 时,再递减保留计数
-
在调用release 之后,对象所占的内存可能会被回收,这样子在调用对象的方法就可能使程序崩溃,这里 “可能” 的意思是对象所占的内存在 “解除分配” (deallocated)之后,只是放回 “可用内存池”(avaiable pool)。若果执行方法时尚未覆写对象,那么对象仍然有效。
-
为避免在不经意间使用无效对象,一般在调用完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 对象。
- autorelease 能延长对象声明周期,使其在跨越方法调用边界后依然可以存活一段时间。
保留环
呈环状相互引用的多个对象,相互持有,这将导致内存泄漏,这里循环中的对象其保留计数不会降为0。
通常采用 “弱引用” 来解决此问题,或者从外界命令某个对象不再保留另外一个对象来打破保留环,从而避免内存泄漏。
引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
第 30 条:以 ARC 简化引用计数
内存泄漏:没有正确的释放已经不再使用的内存。
自用引用计数预先加入适当的保留或释放操作来避免内存泄漏,使用ARC 时,引用计数实际上还是要执行的,只是保留与释放操作是由ARC 自动添加的。
ARC 会自动执行retain、release、autorelease、dealloc等操作,所以在ARC 下调用这些内存管理方法是非法的。因为ARC 会分析何处应该自动调用内存管理方法,所以我们再手动调用的话,会干扰其工作。
实际上,ARC 在调用这些方法时,并不是普通的Objective-C 消息派发机制,而是直接调用其底层的C 语言函数,这样子性能会更好。
使用ARC 时必须遵循的方法命名规则
- 将内存管理语义在方法名中表示出来,若方法名以下列词语开头,则返回的对象归
调用者所有:
alloc
new
copy
mutableCopy
- 将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化。
变量的内存管理语义
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 条:以 “自动释放池块” 降低内存峰值
- 释放对象有两种方式:
一种是调用release 方法,使其保留计数立即递减
一种是调用autorelease 方法,将对象放入 “自动释放池” 中,自动释放池用于存放那些需要稍后某个时刻释放的对象,清空(drain)自动释放池时,系统会向其中的对象发送release 消息。
- 创建自动释放池,系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行 “事件循环”时,都会将其清空。自动释放池于左边花括号创建,并于对应的右花括号自动清空。位于自动释放池范围内的对象,会在末尾处受到release 消息。
@autoreleasepool {
//...
}
-
内存峰值:是指应用程序在某个特定时段内的最大内存用量。
-
对象有可能会放在自动释放池里面,需要等到线程执行下一次事件循环才会清空,这里会导致应用程序所占内存会持续增加,等到临时对象释放的时候,内存用量又会突然下降。我们现在就想把这个内存峰值给降低下来。
-
可以增加一个自动释放池来解决这个问题:这样子对象就会加入到这个释放池,而不是线程的主池中,每次循环都创建和释放这个释放池。
for (int i = 0;i < 100000;i++){
@autorelease{
NSObject *object = [NSObject new];
}
}
-
自动释放池机制就像 “栈” 一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池。
-
对于是否需要用池来优化效率,这个得考虑清楚来,因为自动释放池的创建还是有一丢丢开销的,所以尽量不要建立额外的自动释放池。
自动释放池排布在栈中,对象收到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 条:理解 “块” 这一概念
块可以实现闭包。
块的基础知识
- 块用 “^” 符号来表示,后面跟着一对花括号,括号里面是块的实现代码。块其实就是个值,而且自有其相关类型,可以赋值给变量;块类型的语法和函数指针类似。
^{
//block implementation herer
}
//这里定义了名为someBlock 的变量
//块类型的语法结构如下
//return_type (^block_name)(parameters)
void (^someBlock)() = ^{
//block implementation herer
}
-
在声明块的范围内,所有变量都可以被其捕获。默认情况下被块捕获的变量是不可以在块里修改的,不过可以在声明变量的时候加上__block 修饰符,这样子就可以在块内修改了。
-
如果块所捕获的变量是对象类型,那么就会自动保留它,在系统释放这个块的时候,也会将其一并释放。
-
块总能修改实例变量,所以在声明时无须加__block。不过如果通过读取或写入操作捕获了实例变量,那么也会自动把self 变量一并捕获了,因为实例变量是与self 所指代的实例关联在一起的。
块的内部结构
- 块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class 对象的指针(isa 指针)。
- invoke 变量是这个函数指针,指向块的实现代码。函数原型至少要接受一个void* 型的参数,此参数代表块。为什么要把块对象作为参数传进来呢,因为在执行块的时候,要从内存中把这些捕获到的变量读出来。
descriptor 变量是指向结构体的指针,这个结构体包含块的一些信息。
全局块、栈块及堆块
- 定义块的时候,其所占的内存区域是分配在栈中,意思就是,块只在定义它的那个范围内有效。
void (^block)();
if(***){
block = ^(){
NSLog(@"Block A");
};
}else{
block = ^(){
NSLog(@"Block B");
};
}
block();
/*定义在if else 语句中的两个块都分配在栈内存中,编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块内存覆写掉。所以这里执行block() 有危险。
为了解决这个问题,可以给块发送copy 消息以拷贝之。这样子的话,就可以把块从栈复制到堆可。一旦复制到堆上,块就成了带引用计数的对象了,后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。
*/
- 全局块声明在全局内存里,而且也不能被系统回收,相当于单例。由于运行该块所需的全部信息在编译期确定,所以可以把它作为全局块,这是一种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。
块是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 条:多用派发队列,少用同步锁
- 如果有多个线程要执行同一份代码,那么有时可能会出问题,这种情况下,通常要使用锁来实现某种同步机制。在GCD 出现之前,有两种办法:
采用内置的 “同步块”(synchronization block)
- (void)synchronizedMethod {
@synchronized(self){
//safe
}
}
/*
这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕,执行到代码结尾,锁就释放了。
但是,滥用 @synchronized(self) 则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。
*/
直接使用NSLock 对象,也可以使用NSRecursiveLock “递归锁”,线程能多次持有该锁,而且不会出现死锁。
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
//safe
[_lock unlock];
}
-
对于上面两种方法,有些缺陷,同步块会导致死锁,直接使用锁对象,遇到死锁,就会非常麻烦。
-
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 来执行只需运行一次的线程安全代码
- 对于单例我们创建唯一实例,之前都是用@synchronized 加锁来解决多线程的问题,GCD 提供了一个更加简单的方法来实现。
//单例
+(instancetype)shareInstance{
static dispatch_once_t onceToken;
static PXYAdvertisingPagesHelper *shareInstance;
dispatch_once(&onceToken, ^{
shareInstance = [PXYAdvertisingPagesHelper new];
shareInstance.adTimeout = 5.0;
});
return shareInstance;
}
- 使用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); 造成死锁
-
因为队列有层级关系,所以 “检查当前队列是否为执行同步派发所用的队列” 这种办法,并不是总是奏效的。
-
要解决这个问题,可以通过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 函数用于解决由不可重入的代码引发的死锁,然而能用此函数的解决的问题,通常也能改用 “队列特定数据” 来解决。