[OC学习笔记]系统框架
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]系统框架相关的知识,希望对你有一定的参考价值。
一、熟悉系统框架
编写OC应用程序时几乎都会用到系统框架,其中提供了许多编程中经常使用的类,比如collection
。若是不了解系统框架所提供的内容,那么就可能会把其中已经实现过的东西又重写一遍。用户升级操作系统后,你所开发的应用程序也可以使用最新版的系统库了。所以说,如果直接使用这些框架中的类,那么应用程序就可以得益于新版系统库所带来的改进,而开发者也就无须手动更新其代码了。
将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。有时为ios平台构建的第三方框架所使用的是静态库(static library),这是因为iOS应用程序不允许在其中包含动态库。这些东西严格来讲并不是真正的框架,然而也经常视为框架。不过,所有iOS 平台的系统框架仍然使用动态库。
在为Mac OS X或iOS 系统开发“带图形界面的应用程序”(graphical application)时,会用到名为Cocoa的框架,在 iOS 上称为Cocoa Touch。其实Cocoa 本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。
开发者会碰到的主要框架就是Foundation,像是NSObject
、NSArray
、NSDictionary
等类都在其中。Foundation框架中的类,使用NS这个前缀,此前缀是在OC语言用作NeXTSTEP操作系统的编程语言时首度确定的。Foundation框架真可谓所有OC应用程序的“基础”若是没有它,那么本书大部分内容就不知所云了。
Foundation框架不仅提供了collection
等基础核心功能,而且还提供了字符串处理这样的复杂功能。比方说,NSLinguisticTagger可以解析字符串并找到其中的全部名词、动词、代词等。简言之,Foundation所提供的功能远远不止那几个基础类。
还有个与Foundation相伴的框架,叫做CoreFoundation。虽然从技术上讲,CoreFoundation框架不是OC框架,但它却是编写OC应用程序时所应熟悉的重要框架 Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API。CoreFoundation与Foundation不仅名字相似,而且还有更为紧密的联系。有个功能叫做“无缝桥接”(toll- iree bridging),可以把CoreFoundation中的C语言数据结构平滑转换为Foundation中的 OC对象,也可以反向转换。比方说,Foundation框架中的字符串是NSString,而它可以转换为CoreFoundation里与之等效的CFString对象。无缝桥接技术是用某些相当复杂的代码实现出来的,这些代码可以使运行期系统把CoreFoundation框架中的对象视为普通 Objective-c对象。但是,像无缝桥接这么复杂的技术,想自己编写代码实现它,可不太容易。开发程序时可以使用此功能,但若决定以手工编码的方式来复刻这套机制,则需认真视自己的想法了。
除了Foundation与CoreFoundation之外,还有很多系统库,其中包括但不限于下面列出的这些:
- CFNetwork 此框架提供了C语言级别的网络通信能力,它将BSD 套接字”(BSD socket)抽象成易于使用的网络接口。而Foundation 则将该框架里的部分内容封装为 OC 语言的接口,以便进行网络通信,例如可以用 NSURLConnection 从URI中下载数据。
- CoreAudio 该框架所提供的C语言API可用来操作设备上的音频硬件。这个框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套API可以抽象出另外一套 OC式API,用后者来处理音频问题会更简单些。
- AVFoundation 此框架所提供的OC对象可用来回放并录制音频及视频,比如能够在UI视图类里播放视频。
- CoreData 此框架所提供的OC接口可将对象放入数据库,便于持久保存。 CoreData会处理数据的获取及存储事宜,而且可以跨越Mac OS X及iOS平台。
- CoreText 此框架提供的C语言接口可以高效执行文字排版及染操作。
除此之外,还有别的框架,然而通过此处列出的这几个框架,可以看出OC编程的一项重要特点,那就是经常需要使用底层的C语言级API。用C语言来实现API的好处是,可以绕过OC的运行期系统,从而提升执行速度。当然,由于ARC 只负责 OC的对象,所以使用这些API时尤其需要注意内存管理问题。若想使用这种框架,一定得熟悉C语言基础才行。
大家可能会编写使用UI框架的Mac OS X或iOS应用程序。这两个平台的核心UI框架分别叫做AppKit与UIKit,它们都提供了构建在Foundation与CoreFoundation 之上的OC类。框架里含有UI元素,也含有粘合机制,令开发者可将所有相关内容组装为应用程序。在这些主要的UI框架之下,是CoreAnimation与CoreGraphics 框架。
CoreAnimation 是用OC 语言写成的,它提供了一些工具,而UI 框架则用这些工具来渲染图形并播放动画。开发者编程时可能从来不会深入到这种级别,不过知道该框架总是好的。CoreAnimation 本身并不是框架,它是QuartzCore 框架的一部分。然而在框架的国度里,CoreAnimation仍应算作“一等公民”(first-class citizen)。
CoreGraphics 框架以C语言写成,其中提供了 2D 渲染所必备的数据结构与函数。例如,其中定义了CGPoint、CGSize、CGRect 等数据结构,而UIKit 框架中的 UIView 类在确定视图控件之间的相对位置时,这些数据结构都要用到。
还有很多框架构建在U 框架之上,比方说MapKit 框架,它可以为iOS 程序提供地图功能。又比如 Social 框架,它为Mac OS X及iOS程序提供了社交网络(social networking)功能。开发者通常会将这些框架与操作系统平台所对应的核心UI框架结合起来使用。
总的来说,许多框架都是安装Mac OS X与iOS系统时的标准配置。所以,在打算编写新的工具类之前,最好在系统框架里搜一下,通常都有写好的类可供直接使用。
二、多用块枚举,少用for循环
在编程中经常需要列举collection
中的元素,当前的 OC 语言有多种办法实现此功能,可以用标准的C语言循环,也可以用 OC 1.0 的 NSEnumerator 以及 OC 2.0的快速遍历(fast enumeration)。语言中引入“块”这一特性后,又多出来几种新的遍历方式,而这几种方式容易为开发者所忽视。采用这几种新方式遍历collection
时,可以传入块而collection
中的每个元素都可能会放在块里运行一遍,这种做法通常会大幅度简化编码过程,笔者下面将会详细说明。
本条所讲的 collection
包含 NSArray
、NSDictionary
、NSSet
这几个频繁使用的类型。此外,这里所说的遍历技巧也适用于自定义的 collection
,但是具体做法并不在本条范围内。
(一)for循环
遍历数组的第一种办法就是采用老式的for循环,这令人想起:在作为OC根基的C语言里,就已经有此特性了。这是个很基本的办法,因而功能非常有限。通常会这样写代码:
NSArray *anArray/* ...*/;
for (int i = 0; i < anArray.count; i++)
id object anArray[i];
// Do something with 'object'
这么写还好,不过若要遍历字典或set,就要复杂一些了:
// Dictionary
NSDictionary *aDictionary = /*...*/;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++)
id key = keys[i];
id value = aDictionary[key];
//Do something with 'key' and 'value
// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allobjects];
for (int i = 0; i < objects.count; i++)
id object = objects[i];
// Do something with 'object'
根据定义,字典与set
都是“无序的”(unordered),所以无法根据特定的整数下标来直接访问其中的值。于是,就需要先获取字典里的所有键或是set
里的所有对象,这两种情况下都可以在获取到的有序数组上遍历,以便借此访问原字典及原set 中的值。创建这个附加数组会有额外开销,而且还会多创建一个数组对象,它会保留collection
中的所有元素对象。当然了,释放数组时这些附加对象也要释放,可是要调用本来不需执行的方法。其他各种历方式都无须创建这种中介数组。
for
循环也可以实现反向遍历,计数器的值从“元素个数减1”开始,每次迭代时递减直到0为止。执行反向遍历时,使用for
循环会比其他方式简单许多。
(二)使用OC 1.0 的NSEnumerator来遍历
NSEnumerator 是个抽象基类,其中只定义了两个方法,供其具体子类(concrete subclass)来实现:
- (NSArray*)allobjects
- (id)nextobject
其中关键的方法是nextObject
,它返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。等到枚举中的全部对象都已返回之后,再调用就将返回nil
,这表示达到枚举末端了。
Foundation框架中内建的collection
类都实现了这种遍历方式。例如,想遍历数组,可以这样写代码:
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while ((object [enumerator nextobject]) != nil)
// Do something with 'object"
这种写法的功能与标准的for
循环相似,但是代码却多了一些。其真正优势在于:不论遍历哪种collection
,都可以采用这套相似的语法。比方说,遍历字典及set
时也可以按照这种写法来做:
//Dictionary
NSDictionary *aDictionary = /*...*/;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key[enumerator nextObject]) != nil)
id value = aDictionary[key];
// Do something with 'key' and 'value
// Set
NSSet *aSet = /*...*/;
NSEnumerator *enumerator = [aSet objectEnumerator];
id object;
while ((object = [enumerator nextobject]) != nil)
// Do something with 'object!
遍历字典的方式与数组和set
略有不同,因为字典里既有键也有值,所以要根据给定的键把对应的值提取出来。使用NSEnumerator还有个好处,就是有多种“枚举器”(enumerator)可供使用。比方说,有反向遍历数组所用的枚举器,如果拿它来遍历,就可以按反方向来迭代 collection
中的元素了。例如:
NSArray *anArray = /*...*/;
NSEnumerator *enumeratorm = [anArray reverseObjectEnumerator];
id object;
while ((object = [enumerator nextobject]) != nil)
// Do something with 'object'
与采用for
循环的等效写法相比,上面这段代码读起来更顺畅。
(三)快速遍历
OC 2.0引入了快速遍历这一功能。快速遍历与使用NSEnumcrator 来遍历差不多,然而语法更简洁,它为for
循环开设了in
关键字。这个关键字大幅简化了遍历collection
所需的语法,比方说要遍历数组,就可以这么写:
NSArray *anArray = /*...*/;
for (id object in anArray)
// Do something with 'object'
这样写简单多了。如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为 NSFastEnumeration 的协议,从而令开发者可以采用此语法来迭代该对象。此协议只定义了一个方法:
- (NSUInteger)countByEnumeratingWithstate: (NSFastEnumerationState*)state objects:(id*)stackbuffer count:(NSUInteger)length
该方法的工作原理不在本条目所述范围内。不过网上能找到一些优秀的教程,它们会把这个问题解释得很清楚。其要点在于:该方法允许类实例同时返回多个对象,这就使得循环遍历操作更为高效了。
遍历字典与 set也很简单:
//Dictionary
NSDictionary *aDictionary = /*... */;
for (id key in aDictionary)
id value = aDictionary[key];
// Do something with 'key' and 'value"
// Set
NSSet *aSet = /*...*/;
for (id object in aSet)
//Do something with 'object'
由于NSEnumerator对象也实现了NSFastEnumeration协议,所以能用来执行反向遍历。若要反向遍历数组,可采用下面这种写法:
NSArray *anArray = /*...*/;
for (id object in [anArray reverseObjectEnumerator])
// Do something with 'object"
在目前所介绍的遍历方式中,这种办法是语法最简单且效率最高的,然而如果在遍历字典时需要同时获取键与值,那么会多出来一步。而且,与传统的for
循环不同,这种遍历方式无法轻松获取当前遍历操作所针对的下标。遍历时通常会用到这个下标,比如很多算法都需要它。
(四)基于块的遍历方式
在当前的OC语言中,最新引人的一种做法就是基于块来遍历。NSArray 中定义了下面这个方法,它可以实现最基本的遍历功能:
- (void)enumerateObjectsUsingBlock(void(^)(id object, NSUInteger idx, BOOL *stop))block
除此之外,还有一系列类似的遍历方法,它们可以接受各种选项。以控制遍历操作,稍后将会讨论那些方法。
在遍历数组及set
时,每次迭代都要执行由block
参数所传入的块,这个块有三个参数分别是当前迭代所针对的对象、所针对的下标,以及指向布尔值的指针。前两个参数的含义不言而喻。而通过第三个参数所提供的机制,开发者可以终止遍历操作。
例如,下面这段代码用此方法来遍历数组:
NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:
^(id object, NSUInteger idx, BOOL *stop)
//Do something with 'object
if (shouldStop)
*stop=YES;
];
这种写法稍微多了几行代码,不过依然明晰,而且遍历时既能获取对象,也能知道其下标。此方法还提供了一种优雅的机制,用于终止遍历操作,开发者可以通过设定stop
变量值来实现,当然,使用其他几种遍历方式时,也可以通过break
来终止循环,那样做也很好。
此方式不仅可用来遍历数组。NSSet里面也有同样的块枚举方法,NSDictionary也是这样只是略有不同:
- (void)enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id object, Bool *stop))block
因此,遍历字典与 set 也同样简单:
//Dictionary
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeysAndObjectsUsingBlock:
^(id key, id object, Bool *stop)
// Do something with 'key' and 'object
if (shouldStop)
*stop = YES;
];
//Set
NSSet *aSet = /*...*/;
[aSet enumerateObjectsUsingBlock:
^(id object,BOOL *stop)
// Do something with 'object
if (shouldStop)
*stop = YES;
];
此方式大大胜过其他方式的地方在于:遍历时可以直接从块里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序set
(NSOrderedSet)时也一样。而在遍历字费时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。用这种方式遍历字典,可以同时得知键与值,这很可能比其他方式快很多,因为在字典内部的数据结构中,键与值本来就是存储在一起的。
另外一个好处是,能够修改块的方法签名,以免进行类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给块方法签名来做。比方说,要用“快速遍历法”来遍历字典。若已知字典中的对象必为字符串,则可以这样编码:
for (NSString *key in aDictionary)
NSString *object = (NSString*)aDictionary[key]
// Do something with 'key' and 'object
如果改用基于块的方式来遍历,那么就可以在块方法签名中直接转换:
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndobjectsUsingBlock:
^(NSString *key, NSString *obj, BOOL *stop)
// Do something with 'key' and 'obj'
];
之所以能如此,是因为id类型相当特殊,它可以像本例这样,为其他类型所覆写。要是原来的块签名把键与值都定义成NSObject*
,那这么写就不行了。此技巧初看不甚显眼,实则相当有用。指定对象的精确类型之后,编译器就可以检测出开发者是否调用了该对象所不具备的方法,并在发现这种问题时报错。如果能够确知某collection
里的对象是什么类型,那就应该使用这种方法指明其类型。
用此方式也可以执行反向遍历。数组、字典、set 都实现了前述方法的另一个版本,使开发者可向其传人“选项掩码”(option mask):
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id obj, NSUInteger idx, bool *stop))block
- (void)enumerateKeysAndObjectsWithoptions:(NSEnumerationOptions)options usingBlock:
(void(^)(id key, id obj, bool *stop))block
NSEnumerationOptions类型是个enum
,其各种取值可用“按位或”(bitwise OR)连接,用以表明遍历方式。例如,开发者可以请求以并发方式执行各轮迭代,也就是说,如果当前系统资源状况允许,那么执行每次迭代所用的块就可以并行执行了。通过NSEnumerationConcurrent选项即可开启此功能。如果使用此选项,那么底层会通过GCD来处理并发执行事宜,具体实现时很可能会用到dispatch group。不过,到底如何来实现,不是本条所要讨论的内容。反向遍历是通过NSEnumerationReverse 选项来实现的。要注意:只有在遍历数组或有序 set
等有顺序的 collection
时,这么做才有意义。
总体来看,块枚举法拥有其他遍历方式都具备的优势,而且还能带来更多好处。与快速遍历法相比,它要多用一些代码,可是却能提供遍历时所针对的下标,在遍历字典时也能同时提供键与值,而且还有选项可以开启并发迭代功能,所以多写这点代码还是值得的。
三、对自定义其内存管理语义的collection使用无缝桥接
OC 的系统库包含相当多的collection
类,其中有各种数组、各种字典、各种 set
。Foundation框架定义了这些collection
及其他各种collection
所对应的OC类。与之相似,CoreFoundation 框架也定义了一套C语言 API,用于操作表示这些collection
及其他各种collection
的数据结构。例如,NSArray是Foundation 框架中表示数组的OC类,而CFArray 则是CoreFoundation 框架中的等价物。这两种创建数组的方式也许有区别,然而有项强大的功能可在这两个类型之间平滑转换,它就是“无缝桥接”(toll-free bridging)。
使用“无缝桥接”技术,可以在定义于Foundation 框架中的OC类和定义于 CoreFoundation框架中的C数据结构之间互相转换。笔者将C语言级别的API 称为数据结构,而没有称其为类或对象,这是因为它们与OC 中的类或对象并不相同。例如, CFArray要通过CFArrayRef
来引用,而这是指向struct__CFArray
的指针。CFArrayGetCount
这种函数则可以操作此struct
,以获取数组大小。这和OC中的对应物不同,在OC中,可以创建NSArray对象,并在该对象上调用count
方法,以获取数组大小。
下列代码演示了简单的无缝桥接:
NSArray *anNSArray = @[@1,@2,@3,@4,@5];
CFArrayRef aCFArray = (__bridge CFArrayRef)annSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
//Output: Size of array = 5
转换操作中的__bridge
告诉ARC如何处理转换所涉及的OC对象。__bridge
本身的意思是:ARC仍然具备这个OC对象的所有权。而__bridge_retained
则与之相反,意味着ARC将交出对象的所有权。若是前面那段代码改用它来实现那么用完数组之后就要加上CFRelease
(aCFArray)以释放其内存。与之相似,反向转换通过__bridge_transfer
来实现。比方说,想把CFArrayRef
转换为NSArray*
,并且想令ARC获得对象所有权,那么就可以采用此种转换方式。这三种转换方式称为“桥式转换”(bridge cast)。
可是,你也许会问:以纯OC来编写应用程序时,为何要用到这种功能呢?是因为:Foundation框架中的OC类所具备的某些功能,是CoreFoundation框架中C语言数据结构所不具备的,反之亦然。在使用Foundation框架中的字典对象时会遇到一个大问题,那就是其键的内存管理语义为“拷贝”,而值的语义却是“保留”。除非使用强大的无缝桥接技术,否则无法改变其语义。
CoreFoundation框架中的字典类型叫做CFDictionary。其可变版本称为CFMutable Dictionary。创建CFMutableDictionary时,可以通过下列方法来指定键和值的内存管理语义
CFMutableDictionaryRef CFDictionaryCreateMutable (
CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
)
首个参数表示将要使用的内存分配器(allocator)。如果你大部分时间都在编写OC代码,那么也许会对CoreFoundation 框架中的这部分稍感陌生。CoreFoundation对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存。开发者通常为这个参数传人NULL,表示采用默认的分配器。
第二个参数定义了字典的初始大小。它并不会限制字典的最大容量,只是向分配器提示了一开始应该分配多少内存。假如要创建的字典含有10个对象,那就向该参数传入10。
最后两个参数值得注意。它们定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。这两个参数都是指向结构体的指针,二者所对应的结构体如下:
struct CFDictionaryKeyCallBacks
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CFDictionaryHashCallBack hash;
;
struct CFDictionaryValueCallBacks
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
;
version
参数目前应设为0。当前编程时总是取这个值,不过将来苹果公司也许会修改此结构体,所以要预留该值以表示版本号。这个参数可以用于检测新版与旧版数据结构之间是否兼容。结构体中的其余成员都是函数指针,它们定义了当各种事件发生时应该采用哪个函数来执行相关任务。比方说,如果字典中加人了新的键与值,那么就会调用retain
函数。此参数的类型定义如下:
typedef const void* (*CFDictionaryRetainCallBack) (
CFAllocatorRef allocator,
const void *value
);
由此可见,retain
是个函数指针,其所指向的函数接受两个参数,其类型分别是 FAllocatorRef
与const void*
。传给此函数的value
参数表示即将加入字典中的键或值。而返回的void*
则表示要加到字典里的最终值。开发者可以用下列代码来实现这个回调函数:
const void* CustomCallback(CFAllocatorRef allocator, const void *value)
return value;
这么写只是把即将加入字典中的值照原样返回。于是,如果用它充当retain
回调函数来创建字典,那么该字典就不会“保留”键与值了。将此种写法与无缝桥接搭配起来,就可以创建出特殊的NSDictionary对象,而其行为与用OC创建出来的普通字典不同。
下列范例代码完整演示了这种字典的创建步骤:
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
const void* MyRetainCallback(CFAllocatorRef allocator, const void *value
return CFRetain(value);
void MyReleaseCallback(CFAllocatorRefallocator, const void *value)
CFRelease(value);
CFDictionaryKeyCallbacks =
0,
MyRetainCallback,
MyReleaseCallback,
NULL,
CFEqual CFHash
;
CFDictionaryValueCallBacks valueCallbacks =
0,
MyRetainCallback,
MyReleaseCallback,
NULL,
CFEqual
;
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary*)aCFDictionary;
在设定回调函数时,copyDescription
取值为NULL
,因为采用默认实现就很好而 equal
与hash
回调函数分别设为CFEqual
与CFHash
,因为这二者所采用的做法与 NSMutableDictionary
的默认实现相同。CFEqual
最终会调用 NSObject
的“ isEqual:
”方法而 CFHash
则会调用hash
方法。由此可以看出无缝桥接技术更为强大的一面。
键与值所对应的retain
与release
回调数指针分别指向MyRetainCallback
与MyReleaseCallback
函数。前面说过,在向NSMutableDictionary
中加入键和值时,字典会自动“拷贝”键并“保留”值。如果用作键的对象不支持拷贝操作,那会如何呢?此时就不能使用普通的NSMutableDictionary
了,假如用了,会导致下面这种运行期错误:
** Terminating appdue to uncaught exception
'NSInvalidArgumentException', reason: '-[MyClass
copyWithZone:]:unrecognized selector sent to instance0x7fd069c080b0
该错误表明,对象所属的类不支持NSCopying
协议,因为“copyWithZone:
”方法未实现。开发者可以直接在CoreFoundation 层创建字典,于是就能修改内存管理语义,对键执行“保留”而非“拷贝”操作了。
通过类似手段,也可创建出不保留其元素对象的数组或set。这么做或许有用,因为有时如果令数组保留对象的话,那么可能会引人“保留环”。不过要注意,这个问题可以改用更好的办法来解决。不保留其元素对象的那种数组,很容易出错。要是数组中的某个对象已为系统所回收,而应用程序又去访问该对象的话,那很可能就崩溃了。
四、构建缓存时选用NSCache而非NSDictionary
开发 Mac OS X或iOS应用程序时,经常会遇到一个问题,那就是从因特网下载的图片应如何来缓存。首先能想到的好办法就是把内存中的图片保存到字典里,这样的话,稍后使用时就无须再次下载了。有些程序员会不假思索,直接使用NSDictionary 来做(准确来说,是使用其可变版本),因为这个类很常用。其实,NSCache类更好,它是 Foundation 框架专为处理这种任务而设计的。
NSCache 胜过NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”(low memory)通知时手工删减缓存。而NSCache则会自动删减,由于其是Foundation 框架的一部分,所以与开发者相比,它能在更深的层面上插入挂钩。此外,NSCache 还会先行删减“最久未使用的”(lease recently used)对象。若想自己编写代码来为字典添加此功能,则会十分复杂。
NSCache 并不会“拷贝”键,而是会“保留”它。此行为用NSDictionary也可以实现,然而需要编写相当复杂的代码。NSCache对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。因此,NSCache不会自动拷贝键,所以说,在键不支持拷贝操作的情况下,该类用起来比字典更方便。另外,NSCache是线程安全的。而NSDictionary则绝对不具备此优势,意思就是:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问NSCache。对缓存来说,线程安全通常很重要,因为开发者可能要在某个线程中读取数据,此时如果发现缓存里找不到指定的键,那么就要下载该键所对应的数据了。而下载完数据之后所要执行的回调函数,有可能会放在背景线程中运行,这样的话,就等于是用另外一个线程来写入缓存了。
开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”(overall cost)。开发者在将对象加入缓存时,可为其指定“开销值”。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。然而要注意,“可能”会删减某个对多并不意味着“一定”会删减这个对象。删减对象时所遵照的顺序。由具体实现来定。这尤说明:想通过调整“开销值”来迫使缓存优先删减某对象,不是个好主意。
向缓存中添加对象时,只有在能很快计算出“开销值”的情况下,才应该考虑采用个尺度。若计算过程很复杂,那么照这种方式来使用缓存就达不到最佳效果了,因为每次向缓存中放入对象时,还要专门花时间来计算这个附加因素的值。而缓存的本意则是要增加用程序响应用户操作的速度。比方说,如果计算“开销值”时必须访问磁盘才能确定文件小,或是必须访问数据库才能决定具体取值,那就不太好了。然而,如果要加入缓存中的是 NSData 对象,那么就不妨指定“开销值”了,可以把数据大小当作“开销值”来用。因为 NSData对象的数据大小是已知的,所以计算“开销值”的过程只不过是读取一项属性。
下面这段代码演示了缓存的用法:
#import <Foundation/Foundation.h>
//Network fetcher class
typedef void(^MyNetworkFetcherCompletionHandler)(NSData *data)
@interface MyNetworkFetcher : NSObject
-(id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(MyNetworkFetcherCompletionHandler)handler
@end
//Class that uses the network fetcher and caches results
@interface MyClass :NSObject
@end
@implementation MyClass
NSCache *_cache;
- (id)init
if ((self = [super init]))
_cache = [NSCache new];
//Cache a maximum of 100 URLS
_cache.countLimit = 100;
/*
The size in bytes of data is used as the cost
so this sets a cost limit of 5MB.
*/
cache.totalCostLimit = 5 * 1024 * 1024;
return self;
- (void)downloadDataForURL:(NSURL*)url
NSData *cachedData = [_cache objectForKey:url];
if (cachedData)
// Cache hit
[self useData:cachedData];
else
//Cache miss
MyNetworkFetcher *fetcher = [[MyNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data)
[_cache setobject:data forKey:url cost:data.length];
[self useData:data];
];
@end
在本例中,下载数据所用的URL,就是缓存的键。若缓存未命中(缓存中没有所需的数据,cache miss),则下载数据并将其放入缓存。而数据的“开销值”则设为其长度。创建NSCache 时,将其中可缓存的总对象数目上限设为100,将“总开销上限设为5MB,不过,由于“开销值”以“字节”为单位,所以要通过算式将MB换算成字节。 以上是关于[OC学习笔记]系统框架的主要内容,如果未能解决你的问题,请参考以下文章
还有个类叫做NSPurgeableData,和NSCache搭配起来用,效果很好,此类是NSMutableData的子类,而且实现了NSDiscardableContent协议。如果某个对象所占的内存能够根据需要随时丢弃,那