您的位置:首页 > 移动开发 > IOS开发

[编写高质量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引用,保证实例在执行器继续存活。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息