標籤:
[編寫高品質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引用,保證執行個體在執行器繼續存活。
[編寫高品質iOS代碼的52個有效方法](十一)系統架構