目录
[通过Dispatch Group机制,根据系统资源状况来执行任务](#通过Dispatch Group机制,根据系统资源状况来执行任务)
使用dispatch_once来执行只需运行一次的线程安全代码
不要使用dispatch_get_current_queue
内存管理
理解引用计数
OC使用引用计数来管理内存。
引用计数工作原理
NSObject协议声明了下面三个方法来操作计数器,以递增或递减其值:
retain 递增保留计数
release 递减保留计数
autorelease 待清理"自动释放池",再递减保留计数
对象被创建后保留计数的变化过程如下图所示:

一般调用完release后都会清空指针,这就能保证不会出现可能指向无效对象的指针。
自动释放池
有时可以不调用release,改为调用autorelease,此方法会在稍后递减计数,通常是下一次"事件循环"时递减。
使用自动释放池autorelease可以延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。
保留环
引用计数经常要注意的一个问题是"保留环","保留环"会导致内存泄漏,因为循环中的对象保留计数不会降为0,通常采用"若引用"来解决这一问题。

以ARC简化引用计数
ARC,即自动引用计数,所做的事情就是自动管理引用计数。由于ARC自动执行retain、release、autorelease等操作,所以不能调用上述方法。
ARC在调用这些方法时,并不通过消息派发机制,而是直接调用其底层C语言版本,这样性能更好。
使用ARC时必须遵循的方法命名规则
若方法名以下列词语开头,则其返回的对象归调用者所有:

若方法名不以上述四个词语开头,则表示其所返回的对象不归调用者所有。
变量的内存管理语义
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:


通过__weak局部变量可以打破由块所引入的"保留环"。
ARC如何清理实例变量
ARC会借助OC特性来生成清理例程,来自动清理OC对象,所以ARC环境下通常无需再编写dealloc方法
不过如果有非OC的对象,那么仍然需要清理,dealloc方法可以这样写:

在dealloc方法中只释放引用并解除监听
对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc方法了。dealloc方法中要做的主要就是释放对象所拥有的引用。如果对象拥有其他非OC对象,那就要手工释放。
dealloc方法中还要把配置的观测行为清理掉,比如用NSNotificationCenter订阅的通知,dealloc可以这样写:

有一些开销较大或系统内稀缺的资源不在dealloc中释放,比如文件描述符、套接字、大块内存等。通常的做法是:实现另一个方法,当对象使用完后,就调用此方法。
这种对象所属的类,接口可以这样写:
objectivec
#import <Foudation/Foudation.h>
@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end
该类与开发者的约定是:想打开连接,就调用"open:"方法,连接完毕就调用close方法。
close与dealloc方法可以这样写:
objectivec
#import <Foudation/Foudation.h>
@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end
还要注意:dealloc中不要随便调用其他方法,执行异步任务的方法不应在dealloc里调用,只能在正常状态下执行的方法不应在dealloc里调用,属性的存取方法不应在dealloc里调用。
编写"异常安全代码"时留意内存管理问题
在try块中,如果先保留了某个对象,在释放它以前又抛出了异常,那么除非catch块能处理此问题,否则对象所占内存就将泄漏。
在手动管理引用计数时,要使用@finally块,并在块中释放对象,这样无论是否抛出异常,其中的代码都保证会运行且只运行一次。
当自动管理引用计数时,需要打开-fobjc-arc-exceptions标志,该标志默认关闭,只有在打开时,才会自动管理异常的引用计数,不过降低运行效率。
当遇到大量异常捕获操作时,就要考虑重构代码,用NSError式错误信息传递法来取代异常。
以弱引用避免保留环
最简单的保留环就是两个对象互相引用对方。
保留环会导致内存泄漏
如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。

避免保留环最佳方式是弱引用,这种引用经常用来表示"非拥有关系",将属性声明为unsafe_unretained或weak即可。
两者都会对属性弱引用,区别只是对象回收后的行为:

以"自动释放池块"降低内存峰值
嵌套自动释放池,可以控制应用的内存峰值,使其不致过高。
比如这段代码:

如果方法创建临时对象,那这些对象即便不再使用,也依然存活,直到系统将其释放并回收,意味着所有临时对象都要等for循环执行完才会释放。这会导致程序所占内存量持续上涨。
当循环长度取决于用户输入时更如此,比如:

这时,增加一个自动释放池,把循环内的代码包裹在"自动释放池块"中,那么循环中自动释放的对象就会放在这个池,内存峰值就会降低。

用"僵尸对象"调试内存管理问题
Cocoa提供了"僵尸对象"这一功能,当功能启用后,系统会把所有已经回收的实例转化为"僵尸对象",而不会真正回收它们 。这种对象内存不可能遭到覆写,并且收到消息后会抛出异常,准确说明发送过来的消息,并描述回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
将NSZombieEnabled环境变量设为YES,即可开启此功能。开启功能后,系统会修改对象的isa指针,指向特殊的僵尸类,从而使对象变为僵尸对象,他会相应所有选择子,响应方式为:打印一条包含信息内容及其接收者的消息,然后终止应用程序。
不要使用retainCount
ARC中已经废弃了retainCount这一方法,而在不启用ARC时,也不应该使用这一方法。
这个方法之所以无用,原因在于:它所返回的保留计数只是某个给定时间点上的值。
块与大中枢派发
理解"块"这一概念
块可以实现闭包
块的基础知识
块与函数类似,只不过是直接定义在另一个函数里的,和定义它的函数共享同一个范围内的东西,用符号"^"来表示,后面跟一对花括号。
块类型的语法结构如下:

块的强大之处是:在声明它的范围内,所有变量都可以为其所捕获,但是默认情况下,不可以在块里修改。
如果声明时加上__block修饰符,这样就可以在块内修改了。
如果块捕获了对象类型,就会自动保留它,就会导致一个问题,块本身也具有引用计数,这样就很容易导致循环引用。
块的内部结构
栈的内存布局如图:

这里invoke变量是个函数指针,指向块的实现代码,descriptor是指向结构体的指针,结构体中声明了块对象的总体大小,还声明了copy与dispose两个辅助函数对应的函数指针。
全局块、栈块及堆块
定义块时,所占内存区域是分配在栈中的,离开相应范围后,栈内存可能被覆写。
将块对象发送copy消息就可以拷贝,把块从栈复制到堆。此后再调用copy,只会递增块对象引用计数
除了栈块和堆块,还有全局块,这种块不会捕捉任何状态,也不需要,块所使用的内存区域在编译器就已经确定了。这种块可以声明在全局内存中,不需要在栈中创建。
全局块的拷贝是个空操作,它实际上相当于单例。
为常用的块类型创建typedef
块类型的语法结构如下:

为了简化,可以使用typedef为块起一个已读的别名,比如这样:

现在可以直接使用新类型了。
当块作为参数时,也可以简化,比如:

简化后:

使用类型定义还有个好处,就是在打算重构块时,会很方便,只要修改类型定义处即可。
用handler块降低代码分散程度
为用户界面编码时,经常异步执行任务,如果使用委托模式,那么处理数据的代码总是在受委托者处实现,如下图:

而如果改用块来写的话,代码就会变得更加清晰。

如此一来,代码就变得好懂许多了。
除此之外,委托模式还有一个缺点:如果类要分别使用多个获取器下载不同数据,那就得在delegate回调方法里根据传入参数来切换。
如果使用块,就可以将实现代码与委托者内联在一起。
在设计API时,如果用到了handler块,那就可以增加一个参数,使调用者可以通过此参数来决定应该把块安排在哪个队列上执行。
用块引用其所属对象时不要出现保留环
使用块很容易导致"保留环"
有两种常见情况。第一种如下例:

当使用block捕获self时,就可能造成这种情况,这时应该等闭包执行完后,再打破保留环。
另一种情况是互相引用,这时常见的方法是不保留引用,设定一套机制,令获取器对象自己设法保持存活。
多用派发队列,少用同步锁
在GCD出现之前,有两种办法来实现同步机制,第一种是"同步块"(@synchronized),第二种是直接使用NSLock对象。这两种方法都有其缺陷,替代方案就是GCD。
有种简单而高效的办法可以代替同步块或锁对象,那就是使用"串行同步队列"。GCD在底层实现。可以做许多优化。
比如属性的存取:

将属性的访问操作都放在串行队列中,就可以实现同步。
如果把设置方法改为异步派发,就可以进一步优化:

然而,获取方法与设置方法不能并发执行,但多个获取方法可以并发执行。将并发队列与栅栏块结合,可以进一步优化。
在队列中,栅栏块必须单独执行,不能与其他块并行。
实现代码也很简单:
objectivec
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString*)someString{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
多用GCD,少用performSelector系列方法
使用performSelector方法,可以动态绑定消息,如下图:

但是这种方法不仅可能导致内存泄漏,还很难传达一些非对象类型参数。
避免这些限制和缺陷,方法就是使用块,而perform方法提供的那些线程功能,都可以通过在大中枢派发机制中使用块来执行,延后执行可以用dispatch_after来实现,在另一个线城上执行任务可以通过dispatch_sync及dispatch_async来实现。
掌握GCD及操作队列的使用时机
有时使用NSOperationQueue要比使用GCD技术要更合适。
用NSOperationQueue类的"addOperationWithBlock:"方法搭配NSBlockOperation类使用操作队列,语法与GCD非常类似,但使用下来有以下好处:
通过Dispatch Group机制,根据系统资源状况来执行任务
Dispatch group是GCD的一项特性,能够把任务分组。
与dispatch group相关的有以下几个函数:
objectivec
dispatch_group_tdispatch_group_create// 创建分组
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_bloack_t block);//管理分组
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);//管理分组
有两个方法可以用于等待dispatch group执行完毕:
objectivec
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
二者的区别是:前者会阻塞当前线程,而后者只会在完成任务后进行通知。
未必一定要使用dispatch group,使用异步派发加单个队列也可以实现该效果。然而,使用dispatch group,GCD会根据系统资源状况来调度这些任务。若不采用group,则不仅性能有所下降,还需要额外代码来防止死锁。
使用dispatch_once来执行只需运行一次的线程安全代码
在GCD中,有一种很好的方式实现单例,采用以下函数:
objectivec
void dispatch_once(dispatch_once_t *token, dispatch_block_t block);
此函数接受一个参数,只要这个参数相同,那么这个块里的代码便只执行一次。
可以这样实现单例:

这个参数应该声明在static或global作用域中。
不要使用dispatch_get_current_queue
dispatch_get_current_queue这个函数已经被弃用了。使用这个函数时,容易造成死锁。并且这个函数的行为也总是与预计不符。因为队列存在一套层级体系。

所以使用该函数检查当前队列是否为执行同步派发所用的队列,并不总是奏效。这个函数所返回的不一定是API指定的那个,有可能是API内部的那个同步队列。
为了解决这个问题,最好的办法是通过GCD提供的功能来设定"队列特有数据",通过队列特有数据来解决不可重入导致的死锁。
系统框架
熟悉系统框架
许多系统框架可以直接使用,最重要的是Foundation与CoreFoundation。
许多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。
多用枚举块,少用for循环
遍历collection有四种方式,最基本的是for循环,其次是NSEnumerator类和for...in...快速便利法,最新、最先进的是"块枚举法"。
以数组为例,NSArray中定义了这个方法
objectivec
- (void)enumerateObjectsUsingBlock:(void(^)id object, NSUInteger idx, BOOL *stop)block
通过这种方法遍历collection,可以直接从块里获取更多信息。遍历数组时可以知道当前下标,遍历字典可以同时获取键值,还可以修改块的方法签名以免进行类型转换操作。
对自定义其内存管理语义的collection使用无缝桥接
使用无缝桥接技术,可以在Foudation框架中的OC对象与CoreFoudation框架中的C语言数据结构之间来回转换。
使用无缝桥接,可以将collection转换为具备特殊内存管理语义的collection。
比如字典的键为"拷贝",而值为"保留",这时就只能使用无缝桥接来改变语义。
CF框架中的可变字典叫CFMutableDictionary,可通过下列方法来制定键和值的内存管理语义:

构建缓存时选用NSCache而非NSdictionary
构建缓存时,应选用NSCache而非NSdictionary。
NSCache的优势在于:当系统资源快要耗尽时,它可以自动删减缓存。
NSCache对象可以设置上限来限制缓存中的对象总个数和成本。
NSCache经常与NSData和NSPurgeableData搭配使用。将NSPurgeableData与NSCache搭配使用,可以实现自动清除数据的功能。
精简initiakize与load的实现代码
类的初始化有两个方法,分别是load和initialize
load方法的问题在于,执行时系统处于"脆弱状态",在执行时必然执行超类的load方法,如果依赖程序库,那库里所有相关类的load方法必定会先执行,但是根据某个库,无法判定各个类的载入顺序。因此,在load方法里使用其他类是不安全的。
load方法务必实现精简一些,因为整个程序在执行load时都会阻塞。
initialize方法会在程序首次调用该类之前调用且只调用一次。它与load不同之处在于:
1.initiakize为惰性调用
2.执行initialize时,系统处于正常状态,也能确保一定是"线程安全的环境"中执行。
3.如果某个类为实现initialize,就会运行超类的实现代码。
initialize方法在实现时也尽量精简。
别忘了NSTimer会保留其目标对象
使用重复执行模式的计时器,很容易引入"保留环"
如果创建本类的实例,并调用startPolling方法,创建计时器时,由于目标对象是self,所以要保留此实例。然而,实例也保留了计时器。因此就产生了保留环。
这时可以拓展NSTimer的功能,用"块"来打破保留环。不过需要创建分类,将实现代码加入分类中。
objectivec
- (void)startPolling{
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block: ^{
EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
}
这段代码先定义了一个弱引用,令其指向self,然后捕获这个引用,而不是原本的self变量,这样self就不会被计时器所保留块开始执行时,立即生成strong引用,以保证实例在执行期间持续存活。