[编写高质量iOS代码的52个有效方法](十一)系统框架
先睹为快
47.熟悉框架
48.多用块枚举,少用for循环
49.对自定义其内存管理语义的容器使用无缝桥接
50.构建缓存时选用nscache而非nsdictionary
51.精简initialize与load的实现代码
52.别忘了nstimer会保留其目标对象
第47条:熟悉系统框架
将一系列代码封装为动态库,并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。
开发者会碰到的主要框架就是foundation,像是nsobject、nsarray、nsdictionary等类都在其中。foundation框架中的类都使用ns前缀(表示nextstep操作系统,mac os x的基础)
还有个与foundation相伴的框架,叫corefoundation。其中有很多对应foundation框架中功能的c语言api。corefoundation中的c语言数据结构可以与foundation框架中的objective-c对象无缝桥接。
除此之外还有以下常用框架:
cfnetwork 提供c语言级别的网络通信能力
coreaudio 操作设备音频硬件的c语言api
avfoundation 提供objective-c对象来回访并录制音频及视频
coredata 提供objective-c接口将对象放入,便于持久保存
coretext 可以高效执行文字排版及渲染操作的c语言接口
appkit/uikit mac os x/ios应用程序的ui框架
用纯c语言写成的框架与用objective-c写成的一样重要,若想成为优秀的objective-c开发者,应该掌握c语言的核心概念。
第48条:多用块枚举,少用for循环
在中经常需要列举容器中的元素,当前objective-c语言有多种办法实现此功能,首先是老式的for循环。
nsarray *array = /* ... */; for (int i = 0; i < array.count; i++) { id object = array[i]; // do something with 'object' } nsdictionary *dictionary = /* ... */; nsarray *keys = [dictionary allkeys]; for (int i = 0; i < keys.count; i++) { id key = keys[i]; id value = dictionary[key]; // do something with 'key' and 'value' }
这是最基本的方法,因而功能非常有限。由于字典和set都是无序的,所以遍历它们需要额外创建一个数组(本例中为keys)。
第二种方法是使用nsenumerator抽象基类来遍历
nsarray *array = /* ... */; nsenumerator *enumerator = [array objectenumerator]; id object; while ((object = [enumerator nextobject]) != nil) { // do something with 'object' } nsdictionary *dictionary = /* ... */; nsenumerator *enumerator = [dictionary keyenumerator]; id key; while ((key = [enumerator nextobject]) != nil) { id value = dictionary[key]; // do something with 'key' and 'value' }
这种方法与标准for循环相比,优势在于无论遍历哪种容器,语法都十分类似,如果需要反向遍历,也可以获取反向枚举器。
nsarray *array = /* ... */; nsenumerator *enumerator = [array reverseobjectenumerator];
objective-c 2.0引入了快速遍历。与使用nsenumerator类似,而语法更简洁,它为for循环开始了in关键字。
nsarray *array = /* ... */; for (id object in array){ // do something with 'object' } nsdictionary *dictionary = /* ... */; for (id key in dictionary){ id value = dictionary[key]; // do something with 'key' and 'value' }
如果某个类的对象支持快速对象,只需要遵守nsfastenumeration协议,该协议只定义了一个方法:
- (nsuinteger)countbyenumeratingwithstate:(nsfastenumerarionstate*)state object:(id*)stackbuffer count:(nsuinteger)length
由于nsenumerator也实现了nsfastenumeration协议,所以反向遍历可以这样实现:
nsarray *array = /* ... */; for (id object in [array reverseobjectenumerator]){ // do something with 'object' }
这种方法允许类实例同时返回多个对象,使循环更高效。但缺点有两个,一是遍历字典时不能同时获取键和值,需要多一步操作,二是此方法无法轻松获取当前遍历操作所针对的下标(有可能会用到)。
最后一种方法是基于块的遍历,也是最新的方法
nsarray *array; [array enumerateobjectsusingblock:^(id obj, nsuinteger idx, bool *stop) { // do something with 'object' if (shouldstop) { *stop = yes; } }]; nsdictionary *dictionary; [dictionary enumeratekeysandobjectsusingblock:^(id key, id obj, bool *stop) { // do something with 'key' and 'value' if (shouldstop) { *stop = yes; } }];
此方式的优势在于,遍历时可以直接从块里获取更多信息,并且能够通过修改块的方法名,避免进行类型转换操作。若已知字典中的对象必为字符串:
nsdictionary *dictionary; [dictionary enumeratekeysandobjectsusingblock:^(nsstring *key, nsstring *obj, bool *stop) { // do something with 'key' and 'value' }];
当然,此方法也可以传入选项掩码来执行反向遍历
[array enumerateobjectswithoptions:nsenumerationreverse usingblock:^(id obj, nsuinteger idx, bool *stop) { // do something with 'object' }];
在options处传入nsenumerationconcurrent,可开启并行执行功能,通过底层gcd来实现并处理。
第49条:对自定义其内存管理语义的容器使用无缝桥接
无缝桥接可以实现foundation框架中的类和corefoundation框架中的数据结构之间的互相转换。下面是一个简单的无缝桥接:
nsarray *ansarray = @[@1,@2,@3]; cfarrayref acfarray = (__bridge cfarrayref)ansarray; cfrelease(acfarray);
进行转换操作的修饰符共有3个:
__bridge // 不改变对象的原所有权 __bridge_retained // arc交出对象的所有权,手动管理内存 __bridge_transfer // arc获得对象的所有权,自动管理内存
手动管理内存的对象需要用cfretain与cfrelease来保留或释放。
第50条:构建缓存时选用nscache而非nsdictionary
开发ios程序时,有些程序员会将因特网上下载的图片保存到字典中,这样的话稍后使用就无须再次下载了,其实用nscache类更好,它是foundation框架专门为处理这种任务而设计的。
nscache胜于nsdictionary之处在于,当系统资源将要耗尽时,它可以自动删除最久未使用的缓存。nscache并不会拷贝键,而是保留它,在键不支持拷贝操作的情况下,使用更方便。另外nscache是线程安全的,不需要编写加锁代码的情况下,多个线程也可以同时访问nscache。
下面是缓存的用法
#import // 网络数据获取器类 typedef void(^eocnetworkfetchercompletionhandler)(nsdata *data); @interface eocnetworkfetcher : nsobject - (id)initwithurl:(nsurl*)url; - (void)startwithcompletionhandler:(eocnetworkfetchercompletionhandler)handler; @end // 使用获取器及缓存结果的类 @interface eocclass : nsobject @end @implementation eocclass{ nscache *_cache; } - (id)init{ if ((self = [super init])) { _cache = [nscache new]; // 设置缓存的对象数目上限为100,总开销上限为5mb _cache.countlimit = 100; _cache.totalcostlimit = 5 * 1024 * 1024; } return self; } - (void)downloaddataforurl:(nsurl*)url{ // nspurgeabledata为nsmutabledata的子类,采用与内存管理类似的引用计数,当引用计数为0时,该对象占用的内存可以根据需要随时丢弃 nspurgeabledata *cachedata = [_cache objectforkey:url]; if (cachedata) { // 缓存命中 // 引用计数+1 [cachedata begincontentaccess]; // 使用缓存数据 [self usedata:cachedata]; // 引用计数-1 [cachedata endcontentaccess]; }else{ // 缓存未命中 eocnetworkfetcher *fetcher = [[eocnetworkfetcher alloc] initwithurl:url]; [fetcher startwithcompletionhandler:^(nsdata *data) { // 创建nspurgeabledata对象,引用计数+1 nspurgeabledata *purgeabledata = [nspurgeabledata datawithdata:data]; [_cache setobject:purgeabledata forkey:url cost:purgeabledata.length]; // 使用缓存数据 [self usedata:cachedata]; // 引用计数-1 [purgeabledata endcontentaccess]; }]; } } @end
第51条:精简initialize与load的实现代码
有时候类必须先执行某些初始化操作,然后才能正常使用。在objective-c中,绝大多数类都继承自nsobject这个根类,而该类有两个方法可以用来实现这种初始化操作。首先是load方法:
+ (void)load
加入运行期系统中的每个类及分类,都会调用此方法,而且仅调用一次。在ios中,这类方法会在应用程序启动时执行(mac os x中可以使用动态加载,程序启动之后再加载)。在执行load方法时,是先执行超类的load方法,再执行子类的,先执行类的,再执行其所属分类的。如果代码还依赖了其他程序库,则会有限执行该程序库中的load方法。但在给定的某个程序库中,无法判断出各个类的载入顺序。
#import #import "eocclassa.h" // 来自同一个库 @interface eocclassb : nsobject @end @implementation eocclassb + (void)load{ nslog(@"loading eocclassb"); eocclassa *object = [eocclassa new]; // ues object } @end
这段代码不安全,因为无法确定eocclassa已在执行eocclassb load方法时已经加载好了。
load方法不遵从普通方法的继承规则,如果某个类本身没实现load方法,那么不管其超类是否实现此方法,系统都不会调用。
load方法应该尽量精简,因为整个程序执行load方法时都会阻塞。不要在里面等待锁,也不要调用可能会加锁的方法。总之,能不做的事情就别做。
想要执行与类相关的初始化操作,还有个方法,就是重写下列方法
+ (void)initialize
对于每个类来说,该方法会在程序首次调用该类之前调用,而且只调用一次。initialize与load方法主要有3个区别:
1. initialize方法只有当程序用到了相关类才会调用,而load不同,程序必须阻塞并等所有类的load都执行完毕,才能继续。
2. 运行期系统执行initialize方法时,处于正常状态,而不是阻塞状态。为保证线程安全,只会阻塞其他操作该类或类实例的线程。
3. 如果某个类未实现initialize方法,而超类实现了它,那么就会运行超类的方法。
initialize方法也应当尽量精简,只需要在里面设置一些状态,使本类能够正常运作就可以了,不要执行那种耗时太久或需要加锁的任务,也尽量不要在其中调用其他方法,即使是本类的方法。
若某个全局状态无法在编译期初始化,则可以放在initialize里来做。
// eocclass.h #import @interface eocclass : nsobject @end // eocclass.m #import "eocclass.h" static const int kinterval = 10; static nsmutablearray *ksomeobjects; @implementation eocclass + (void)initialize{ // 判断类的类型,防止在子类中执行 if(self == [eocclass class]){ ksomeobjects = [nsmutablearray new]; } } @end
整数可以在编译期定义,然而可变数组不行,下面这样创建对象会报错。
static nsmutablearray *ksomeobjects = [nsmutablearray new];
第52条:别忘了nstimer会保留其目标对象
nstimer(计时器)是一种很方便很有用的对象,计时器要和运行循环相关联,运行循环到时候会触发任务。只有把计时器放到运行循环里,它才能正常触发任务。例如,下面这个方法可以创建计时器,并将其预先安排在当前运行循环中:
+ (nstimer *)scheduledtimerwithtimeinterval:(nstimeinterval)ti target:(id)atarget selector:(sel)aselector userinfo:(id)userinfo repeats:(bool)yesorno;
此方法创建出来的计时器会在指定的间隔时间之后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target和selector表示在哪个对象上调用哪个方法。执行完任务后,一次性计时器会失效,若repeats为yes,那么必须调用invalidate方法才能使其停止。
重复执行模式的计时器,很容易引入保留环:
@interface eocclass : nsobject - (void)startpolling; - (void)stoppolling; @end @implementation eocclass{ nstimer *_politimer; } - (id) init{ return [super init]; } - (void)dealloc{ [_politimer invalidate]; } - (void)stoppolling{ [_politimer invalidate]; _politimer = nil; } - (void)startpolling{ _politimer = [nstimer scheduledtimerwithtimeinterval:5.0 target:self selector:@selector(p_dopoll) userinfo:nil repeats:yes]; } - (void)p_dopoll{ // code }
如果创建了本类实例,并调用了startpolling方法。创建计时器的时候,由于目标对象是self,所以要保留此实例。然而,因为计时器是用实例变量存放的,所以实例也保留了计数器,于是就产生了保留环。
调用stoppolling方法或令系统将实例回收(会自动调用dealloc方法)可以使计时器失效,从而打破循环,但无法确保startpolling方法一定调用,而由于计时器保存着实例,实例永远不会被系统回收。当eocclass实例的最后一个外部引用移走之后,实例仍然存活,而计时器对象也就不可能被系统回收,除了计时器外没有别的引用再指向这个实例,实例就永远丢失了,造成内存泄漏。
解决方案是采用块为计时器添加新功能
@interface nstimer (eocblockssupport) + (nstimer*)eoc_scheduledtimerwithtimeinterval:(nstimeinterval)interval block:(void(^)())block repeats:(bool)repeats; @end @implementation nstimer( eocblockssupport) + (nstimer*)eoc_scheduledtimerwithtimeinterval:(nstimeinterval)interval block:(void (^)())block repeats:(bool)repeats{ return [self scheduledtimerwithtimeinterval:interval target:self selector:@selector(eoc_blockinvoke:) userinfo:[block copy] repeats:repeats]; } + (void)eoc_blockinvoke:(nstimer*)timer{ void (^block)() = timer.userinfo; if (block) { block(); } }
再修改stoppolling方法:
- (void)startpolling{ __weak eocclass *weakself = self; _politimer = [nstimer eoc_scheduledtimerwithtimeinterval:5.0 block:^{ eocclass *strongself = weakself; [strongself p_dopoll]; } repeats:yes]; }
这段代码先定义了一个弱引用指向self,然后用块捕获这个引用,这样self就不会被计时器所保留,当块开始执行时,立刻生成strong引用,保证实例在执行器继续存活。
推荐阅读
-
[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)
-
[编写高质量iOS代码的52个有效方法](十一)系统框架
-
[编写高质量iOS代码的52个有效方法](一)Objective-C基础
-
[编写高质量iOS代码的52个有效方法](五)接口与API设计(下)
-
[编写高质量iOS代码的52个有效方法](八)内存管理(下)
-
编写高质量iOS与OSX代码的52个有效方法-第四章:协议与封装
-
Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法(Matt Galloway著)读书笔记(一)
-
编写高质量iOS与OS X代码的52个有效方法 读后感
-
[编写高质量iOS代码的52个有效方法](十)Grand Central Dispatch(GCD)
-
[编写高质量iOS代码的52个有效方法](十一)系统框架