[编写高质量iOS代码的52个有效方法](十一)系统框架
2016-07-29 16:03
447 查看
[编写高质量iOS代码的52个有效方法](十一)系统框架
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway先睹为快
47.熟悉系统框架48.多用块枚举,少用for循环
49.对自定义其内存管理语义的容器使用无缝桥接
50.构建缓存时选用NSCache而非NSDictionary
51.精简initialize与load的实现代码
52.别忘了NSTimer会保留其目标对象
目录
编写高质量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 <Foundation/Foundation.h> // 网络数据获取器类 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 <Foundation/Foundation.h> #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 <Foundation/Foundation.h> @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引用,保证实例在执行器继续存活。
相关文章推荐
- Objective-C的内省(Introspection)用法小结
- Objective-C中常用的结构体NSRange,NSPoint,NSSize(CGSize),NSRect实例分析
- Objective-C中使用NSString类操作字符串的方法小结
- Objective-C中NSNumber与NSDictionary的用法简介
- Objective-C实现冒泡排序算法的简单示例
- Objective-C中NSLog输出格式大全
- Objective-C实现自定义的半透明导航
- 浅析Objective-C的程序结构及面向对象的编程方式
- Objective-C的入门学习笔记
- Objective-C实现无限循环轮播器
- 全面解析Objective-C中的block代码块的使用
- Swift调用Objective-C编写的API实例
- Swift、Objective-C、Cocoa混合编程设置指南
- Objective-c代码如何移植为Swift代码 Objective-c代码转移到Swift过程介绍
- Swift调用Objective-C代码
- iOS中的NSTimer定时器的初步使用解析
- Objective-C中字符串NSString的常用操作方法总结
- IOS开发代码分享之用nstimer实现倒计时功能
- 以实例讲解Objective-C中的KVO与KVC机制
- 简介Objective-C解析XML与JSON数据格式的方法