你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> [編寫高質量iOS代碼的52個有效方法](十一)系統框架

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

編輯:IOS開發綜合

先睹為快

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 

// 網絡數據獲取器類
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 
#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];

第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引用,保證實例在執行器繼續存活。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved