天天看點

[編寫高品質iOS代碼的52個有效方法](十一)系統架構[編寫高品質iOS代碼的52個有效方法](十一)系統架構

[編寫高品質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 = ; i < array.count; i++) {
    id object = array[i];
    // Do something with 'object'
}

NSDictionary *dictionary = /* ... */;
NSArray *keys = [dictionary allKeys];
for (int i = ; 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協定,該協定隻定義了一個方法:

由于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 = ;
        _cache.totalCostLimit =  *  * ;
    }
    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 = ;
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: 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: block:^{
        EOCClass *strongSelf = weakSelf;
        [strongSelf p_doPoll];
    } repeats:YES];
}
           

這段代碼先定義了一個弱引用指向self,然後用塊捕獲這個引用,這樣self就不會被計時器所保留,當塊開始執行時,立刻生成strong引用,保證執行個體在執行器繼續存活。