47.熟悉系統框架
48.多用塊枚舉,少用for循環
49.對自定義其內存管理語義的容器使用無縫橋接
50.構建緩存時選用NSCache而非NSDictionary
51.精簡initialize與load的實現代碼
52.別忘了NSTimer會保留其目標對象
將一系列代碼封裝為動態庫,並在其中放入描述其接口的頭文件,這樣做出來的東西就叫框架。
開發者會碰到的主要框架就是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語言的核心概念。
在編程中經常需要列舉容器中的元素,當前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來實現並處理。
無縫橋接可以實現Foundation框架中的類和CoreFoundation框架中的數據結構之間的互相轉換。下面是一個簡單的無縫橋接:
NSArray *aNSArray = @[@1,@2,@3]; CFArrayRef aCFArray = (__bridge CFArrayRef)aNSArray; CFRelease(aCFArray);
進行轉換操作的修飾符共有3個:
__bridge // 不改變對象的原所有權 __bridge_retained // ARC交出對象的所有權,手動管理內存 __bridge_transfer // ARC獲得對象的所有權,自動管理內存
手動管理內存的對象需要用CFRetain與CFRelease來保留或釋放。
開發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
有時候類必須先執行某些初始化操作,然後才能正常使用。在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];
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引用,保證實例在執行器繼續存活。