《Effective Objective-C》乾貨三部曲(二):規範篇

J_Knight_發表於2018-01-10

繼上一篇《Effective Objective-C 》乾貨三部曲(一):概念篇之後,本篇即是三部曲的第二篇:規範篇。 沒看過三部曲第一篇的小夥伴可能不知道我在說神馬,在這裡還是先囉嗦一下三部曲是咋回事:筆者將《Effective Objective-C 》這本書的52個知識點分為三大類進行了歸類整理:

  • 概念類:講解了一些概念性知識。
  • 規範類:講解了一些為了避免一些問題或者為後續開發提供便利所需要遵循的規範性知識。
  • 技巧類:講解了一些為了解決某些特定問題而需要用到的技巧性知識。

然後用思維導圖整理了一下:

三部曲分佈圖

作為三部曲的第二篇,本篇總結抽取了《Effective Objective-C 》這本書中講解規範性知識的部分:這些知識點都是為了避免在開發過程中出現問題或給開發提供便利的規範性知識點。掌握這些知識有助於形成科學地寫OC程式碼的習慣,使得程式碼更加容易維護和擴充套件,學習這類知識是iOS初學者進階的必經之路。

第2條: 在類的標頭檔案中儘量少引用其他標頭檔案

有時,類A需要將類B的例項變數作為它公共API的屬性。這個時候,我們不應該引入類B的標頭檔案,而應該使用向前宣告(forward declaring)使用class關鍵字,並且在A的實現檔案引用B的標頭檔案。

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//將EOCEmployer作為屬性

@end

// EOCPerson.m
#import "EOCEmployer.h"

複製程式碼

這樣做有什麼優點呢:

  • 不在A的標頭檔案中引入B的標頭檔案,就不會一併引入B的全部內容,這樣就減少了編譯時間。
  • 可以避免迴圈引用:因為如果兩個類在自己的標頭檔案中都引入了對方的標頭檔案,那麼就會導致其中一個類無法被正確編譯。

但是個別的時候,必須在標頭檔案中引入其他類的標頭檔案:

主要有兩種情況:

  1. 該類繼承於某個類,則應該引入父類的標頭檔案。
  2. 該類遵從某個協議,則應該引入該協議的標頭檔案。而且最好將協議單獨放在一個標頭檔案中。

第3條:多用字面量語法,少用與之等價的方法

1. 宣告時的字面量語法:

在宣告NSNumber,NSArray,NSDictionary時,應該儘量使用簡潔字面量語法。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
複製程式碼
NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};
複製程式碼

2. 集合類取下標的字面量語法:

NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下標操作也應該儘量使用字面量語法。

NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

複製程式碼

使用字面量語法的優點:

  1. 程式碼看起來更加簡潔。
  2. 如果存在nil值,則會立即丟擲異常。如果在不用字面量語法定義陣列的情況下,如果陣列內部存在nil,則系統會將其設為陣列最後一個元素並終止。所以當這個nil不是最後一個元素的話,就會出現難以排查的錯誤。

注意: 字面量語法建立出來的字串,陣列,字典物件都是不可變的。

第4條:多用型別常量,少用#define預處理命令

在OC中,定義常量通常使用預處理命令,但是並不建議使用它,而是使用型別常量的方法。 首先比較一下這兩種方法的區別:

  • 預處理命令:簡單的文字替換,不包括型別資訊,並且可被任意修改。
  • 型別常量:包括型別資訊,並且可以設定其使用範圍,而且不可被修改。

我們可以看出來,使用預處理雖然能達到替換文字的目的,但是本身還是有侷限性的:不具備型別 + 可以被任意修改,總之給人一種不安全的感覺。

知道了它們的長短處,我們再來簡單看一下它們的具體使用方法:

預處理命令:

#define W_LABEL (W_SCREEN - 2*GAP)

這裡,(W_SCREEN - 2*GAP)替換了W_LABEL,它不具備W_LABEL的型別資訊。而且要注意一下:如果替換式中存在運算子號,以筆者的經驗最好用括號括起來,不然容易出現錯誤(有體會)。

型別常量:

static const NSTimeIntervalDuration = 0.3;

這裡: const 將其設定為常量,不可更改。 static意味著該變數僅僅在定義此變數的編譯單元中可見。如果不宣告static,編譯器會為它建立一個外部符號(external symbol)。我們來看一下對外公開的常量的宣告方法:

對外公開某個常量:

如果我們需要傳送通知,那麼就需要在不同的地方拿到通知的“頻道”字串,那麼顯然這個字串是不能被輕易更改,而且可以在不同的地方獲取。這個時候就需要定義一個外界可見的字串常量。

//header file
extern NSString *const NotificationString;

//implementation file
NSString *const  NotificationString = @"Finish Download";
複製程式碼

這裡NSString *const NotificationString是指標常量。 extern關鍵字告訴編譯器,在全域性符號表中將會有一個名叫NotificationString的符號。

我們通常在標頭檔案宣告常量,在其實現檔案裡定義該常量。由實現檔案生成目標檔案時,編譯器會在“資料段”為字串分配儲存空間。

最後注意一下公開和非公開的常量的命名規範:

公開的常量:常量的名字最好用與之相關的類名做字首。 非公開的常量:侷限於某個編譯單元(tanslation unit,實現檔案 implementation file)內,在簽名加上字母k。

第5條:用列舉表示狀態,選項,狀態碼

我們經常需要給類定義幾個狀態,這些狀態碼可以用列舉來管理。下面是關於網路連線狀態的狀態碼列舉:

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};
複製程式碼

需要注意的一點是: 在列舉型別的switch語句中不要實現default分支。它的好處是,當我們給列舉增加成員時,編譯器就會提示開發者:switch語句並未處理所有的列舉。對此,筆者有個教訓,又一次在switch語句中將“預設分支”設定為列舉中的第一項,自以為這樣寫可以讓程式更健壯,結果後來導致了嚴重的崩潰。

第7條: 在物件內部儘量直接訪問例項變數

關於例項變數的訪問,可以直接訪問,也可以通過屬性的方式(點語法)來訪問。書中作者建議在讀取例項變數時採用直接訪問的形式,而在設定例項變數的時候通過屬性來做。

直接訪問屬性的特點:

  • 繞過set,get語義,速度快;

通過屬性訪問屬性的特點:

  • 不會繞過屬性定義的記憶體管理語義
  • 有助於打斷點排查錯誤
  • 可以觸發KVO

因此,有個關於折中的方案:

設定屬性:通過屬性 讀取屬性:直接訪問

不過有兩個特例:

  1. 初始化方法和dealloc方法中,需要直接訪問例項變數來進行設定屬性操作。因為如果在這裡沒有繞過set方法,就有可能觸發其他不必要的操作。
  2. 惰性初始化(lazy initialization)的屬性,必須通過屬性來讀取資料。因為惰性初始化是通過重寫get方法來初始化例項變數的,如果不通過屬性來讀取該例項變數,那麼這個例項變數就永遠不會被初始化。

第15條:用字首 避免名稱空間衝突

Apple宣稱其保留使用所有"兩字母字首"的權利,所以我們選用的字首應該是三個字母的。 而且,如果自己開發的程式使用到了第三方庫,也應該加上字首。

第18條:儘量使用不可變物件

書中作者建議儘量把對外公佈出來的屬性設定為只讀,在實現檔案內部設為讀寫。具體做法是:

在標頭檔案中,設定物件屬性為readonly,在實現檔案中設定為readwrite。這樣一來,在外部就只能讀取該資料,而不能修改它,使得這個類的例項所持有的資料更加安全。

而且,對於集合類的物件,更應該仔細考慮是否可以將其設為可變的。

如果在公開部分只能設定其為只讀屬性,那麼就在非公開部分儲存一個可變型。這樣一來,當在外部獲取這個屬性時,獲取的只是內部可變型的一個不可變版本,例如:

在公共API中:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公開的不可變集合

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;

@end

複製程式碼

在這裡,我們將friends屬性設定為不可變的set。然後,提供了來增加和刪除這個set裡的元素的公共介面。

在實現檔案裡:


@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //實現檔案裡的可變集合
}

- (NSSet*)friends {
     return [_internalFriends copy]; //get方法返回的永遠是可變set的不可變型
}

- (void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person]; //在外部增加集合元素的操作
    //do something when add element
}

- (void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObject:person]; //在外部移除元素的操作
    //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {

     if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
 return self;
}

複製程式碼

我們可以看到,在實現檔案裡,儲存一個可變set來記錄外部的增刪操作。

這裡最重要的程式碼是:

- (NSSet*)friends {
 return [_internalFriends copy];
}
複製程式碼

這個是friends屬性的獲取方法:它將當前儲存的可變set複製了一不可變的set並返回。因此,外部讀取到的set都將是不可變的版本。

等一下,有個疑問:

在公共介面設定不可變set 和 將增刪的程式碼放在公共介面中是否矛盾的?

答案:並不矛盾!

因為如果將friends屬性設定為可變的,那麼外部就可以隨便更改set集合裡的資料,這裡的更改,僅僅是底層資料的更改,並不伴隨其他任何操作。 然而有時,我們需要在更改set資料的同時要執行隱祕在實現檔案裡的其他工作,那麼如果在外部隨意更改這個屬性的話,顯然是達不到這種需求的。

因此,我們需要提供給外界我們定製的增刪的方法,並不讓外部”自行“增刪。

第19條:使用清晰而協調的命名方式

在給OC的方法取名字的時候要充分利用OC方法的命名優勢,取一個語義清晰的方法名!什麼叫語義清晰呢?就是說讀起來像是一句話一樣。

我們看一個例子:

先看名字取得不好的:

//方法定義
- (id)initWithSize:(float)width :(float)height;

//方法呼叫
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithSize:5.0f :10.0f];
複製程式碼

這裡定義了Rectangle的初始化方法。雖然直觀上可以知道這個方法通過傳入的兩個引數來組成矩形的size,但是我們並不知道哪個是矩形的寬,哪個是矩形的高。 來看一下正確的? :

//方法定義
- (id)initWithWidth:(float)width height:(float)height;

//方法呼叫
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithWidth:5.0f height:10.0f];

複製程式碼

這個方法名就很好的詮釋了該方法的意圖:這個類的初始化是需要寬度和高度的。而且,哪個引數是高度,哪個引數是寬度,看得人一清二楚。永遠要記得:程式碼是給人看的

筆者自己總結的方法命名規則:

每個冒號左邊的方法部分最好與右邊的引數名一致。

對於返回值是布林值的方法,我們也要注意命名的規範:

  • 獲取”是否“的布林值,應該增加“is”字首:


- isEqualToString:

複製程式碼

獲取“是否有”的布林值,應該增加“has”字首:


- hasPrefix:

複製程式碼

第20條:為私有方法名加字首

建議在實現檔案裡將非公開的方法都加上字首,便於除錯,而且這樣一來也很容易區分哪些是公共方法,哪些是私有方法。因為往往公共方法是不便於任意修改的。

在這裡,作者舉了個例子:


#import <Foundation/Foundation.h>

@interface EOCObject : NSObject

- (void)publicMethod;

@end


@implementation EOCObject

- (void)publicMethod {
 /* ... */
}

- (void)p_privateMethod {
 /* ... */
}

@end

複製程式碼

注意: 不要用下劃線來區分私有方法和公共方法,因為會和蘋果公司的API重複。

第23條:通過委託與資料來源協議進行物件間通訊

如果給委託物件傳送訊息,那麼必須提前判斷該委託物件是否實現了該訊息:


NSData *data = /* data obtained from network */;

if ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)])
{
        [_delegate networkFetcher:self didReceiveData:data];
}

複製程式碼

而且,最好再加上一個判斷:判斷委託物件是否存在


NSData *data = /* data obtained from network */;

if ( (_delegate) && ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)]))
{
        [_delegate networkFetcher:self didReceiveData:data];
}

複製程式碼

對於代理模式,在iOS中分為兩種:

  • 普通的委託模式:資訊從類流向委託者
  • 資訊源模式:資訊從資料來源流向類

普通的委託 | 資訊源

就好比tableview告訴它的代理(delegate)“我被點選了”;而它的資料來源(data Source)告訴它“你有這些資料”。仔細回味一下,這兩個資訊的傳遞方向是相反的。

第24條:將類的實現程式碼分散到便於管理的數個分類中

通常一個類會有很多方法,而這些方法往往可以用某種特有的邏輯來分組。我們可以利用OC的分類機制,將類的這些方法按一定的邏輯劃入幾個分割槽中。

例子:

無分類的類:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;


/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;


/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;


@end

複製程式碼

分類之後:

#import <Foundation/Foundation.h>


@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;



- (id)initWithFirstName:(NSString*)firstName

lastName:(NSString*)lastName;

@end



@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end



@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end



@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

複製程式碼

其中,FriendShip分類的實現程式碼可以這麼寫:


// EOCPerson+Friendship.h
#import "EOCPerson.h"


@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end


// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"


@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person {
 /* ... */
}

- (void)removeFriend:(EOCPerson*)person {
 /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson*)person {
 /* ... */
}

@end

複製程式碼

注意:在新建分類檔案時,一定要引入被分類的類檔案。

通過分類機制,可以把類程式碼分成很多個易於管理的功能區,同時也便於除錯。因為分類的方法名稱會包含分類的名稱,可以馬上看到該方法屬於哪個分類中。

利用這一點,我們可以建立名為Private的分類,將所有私有方法都放在該類裡。這樣一來,我們就可以根據private一詞的出現位置來判斷呼叫的合理性,這也是一種編寫“自我描述式程式碼(self-documenting)”的辦法。

第25條:總是為第三方類的分類名稱加字首

分類機制雖然強大,但是如果分類裡的方法與原來的方法名稱一致,那麼分類的方法就會覆蓋掉原來的方法,而且總是以最後一次被覆蓋為基準。

因此,我們應該以名稱空間來區別各個分類的名稱與其中定義的方法。在OC裡的做法就是給這些方法加上某個共用的字首。例如:

@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;

@end

複製程式碼

因此,如果我們想給第三方庫或者iOS框架裡的類新增分類時,最好將分類名和方法名加上字首。

第26條:勿在分類中宣告屬性

除了實現檔案裡的class-continuation分類中可以宣告屬性外,其他分類無法向類中新增例項變數。

因此,類所封裝的全部資料都應該定義在主介面中,這裡是唯一能夠定義例項變數的地方。

關於分類,需要強調一點:

分類機制,目標在於擴充套件類的功能,而不是封裝資料。

第27條:使用class-continuation分類 隱藏實現細節

通常,我們需要減少在公共介面中向外暴露的部分(包括屬性和方法),而因此帶給我們的侷限性可以利用class-continuation分類的特性來補償:

  • 可以在class-continuation分類中增加例項變數。
  • 可以在class-continuation分類中將公共介面的只讀屬性設定為讀寫。
  • 可以在class-continuation分類中遵循協議,使其不為人知。

第31條:在dealloc方法中只釋放引用並解除監聽

永遠不要自己呼叫dealloc方法,執行期系統會在適當的時候呼叫它。根據效能需求我們有時需要在dealloc方法中做一些操作。那麼我們可以在dealloc方法裡做什麼呢?

  • 釋放物件所擁有的所有引用,不過ARC會自動新增這些釋放程式碼,可以不必操心。
  • 而且物件擁有的其他非OC物件也要釋放(CoreFoundation物件就必須手動釋放)
  • 釋放原來的觀測行為:登出通知。如果沒有及時登出,就會向其傳送通知,使得程式崩潰。

舉個簡單的? :


- (void)dealloc {

     CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];

}

複製程式碼

尤其注意:在dealloc方法中不應該呼叫其他的方法,因為如果這些方法是非同步的,並且回撥中還要使用當前物件,那麼很有可能當前物件已經被釋放了,會導致崩潰。

並且在dealloc方法中也不能呼叫屬性的存取方法,因為很有可能在這些方法裡還有其他操作。而且這個屬性還有可能處於鍵值觀察狀態,該屬性的觀察者可能會在屬性改變時保留或者使用這個即將回收的物件。

第36條:不要使用retainCount

在非ARC得環境下使用retainCount可以返回當前物件的引用計數,但是在ARC環境下呼叫會報錯,因為該方法已經被廢棄了 。

它被廢棄的原因是因為它所返回的引用計數只能反映物件某一時刻的引用計數,而無法“預知”物件將來引用計數的變化(比如物件當前處於自動釋放池中,那麼將來就會自動遞減引用計數)。

第46條:不要使用dispatch_get_current_queue

我們無法用某個佇列來描述“當前佇列”這一屬性,因為派發佇列是按照層級來組織的。

那麼什麼是佇列的層級呢?

佇列的層及分佈

安排在某條佇列中的快,會在其上層佇列中執行,而層級地位最高的那個佇列總是全域性併發佇列。

在這裡,B,C中的塊會在A裡執行。但是D中的塊,可能與A裡的塊並行,因為A和D的目標佇列是併發佇列。

正因為有了這種層級關係,所以檢查當前佇列是併發的還是非併發的就不會總是很準確。

第48條:多用塊列舉,少用for迴圈

當遍歷集合元素時,建議使用塊列舉,因為相對於傳統的for迴圈,它更加高效,而且簡潔,還能獲取到用傳統的for迴圈無法提供的值:

我們首先看一下傳統的遍歷:

傳統的for遍歷

NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
   id object = anArray[i];
   // Do something with 'object'
}



// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
   id key = keys[i];
   id value = aDictionary[key];
   // Do something with 'key' and 'value'
}


// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
   id object = objects[i];
   // Do something with 'object'

}

複製程式碼

我們可以看到,在遍歷NSDictionary,和NSet時,我們又新建立了一個陣列。雖然遍歷的目的達成了,但是卻加大了系統的開銷。

利用快速遍歷:

NSArray *anArray = /* ... */;
for (id object in anArray) {
 // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
 id value = aDictionary[key];
 // Do something with 'key' and 'value'

}


NSSet *aSet = /* ... */;
for (id object in aSet) {
 // Do something with 'object'
}

複製程式碼

這種快速遍歷的方法要比傳統的遍歷方法更加簡潔易懂,但是缺點是無法方便獲取元素的下標。

利用基於block的遍歷:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){

   // Do something with 'object'
   if (shouldStop) {
      *stop = YES; //使迭代停止
  }

}];


“// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
     // Do something with 'key' and 'object'
     if (shouldStop) {
        *stop = YES;
    }
}];


// Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
     // Do something with 'object'
     if (shouldStop) {
        *stop = YES;
    }
複製程式碼

我們可以看到,在使用塊進行快速列舉的時候,我們可以不建立臨時陣列。雖然語法上沒有快速列舉簡潔,但是我們可以獲得陣列元素對應的序號,字典元素對應的鍵值,而且,我們還可以隨時令遍歷終止。

利用快速列舉和塊的列舉還有一個優點:能夠修改塊的方法簽名

for (NSString *key in aDictionary) {
         NSString *object = (NSString*)aDictionary[key];
        // Do something with 'key' and 'object'
}
複製程式碼

NSDictionary *aDictionary = /* ... */;

    [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){

             // Do something with 'key' and 'obj'

}];

複製程式碼

如果我們可以知道集合裡的元素型別,就可以修改簽名。這樣做的好處是:可以讓編譯期檢查該元素是否可以實現我們想呼叫的方法,如果不能實現,就做另外的處理。這樣一來,程式就能變得更加安全。

第50條:構建快取時選用NSCache 而非NSDictionary

如果我們快取使用得當,那麼應用程式的響應速度就會提高。只有那種“重新計算起來很費事的資料,才值得放入快取”,比如那些需要從網路獲取或從磁碟讀取的資料。

在構建快取的時候很多人習慣用NSDictionary或者NSMutableDictionary,但是作者建議大家使用NSCache,它作為管理快取的類,有很多特點要優於字典,因為它本來就是為了管理快取而設計的。

NSCache優於NSDictionary的幾點:

  • 當系統資源將要耗盡時,NSCache具備自動刪減緩衝的功能。並且還會先刪減“最久未使用”的物件。
  • NSCache不拷貝鍵,而是保留鍵。因為並不是所有的鍵都遵從拷貝協議(字典的鍵是必須要支援拷貝協議的,有侷限性)。
  • NSCache是執行緒安全的:不編寫加鎖程式碼的前提下,多個執行緒可以同時訪問NSCache。

關於操控NSCache刪減內容的時機

開發者可以通過兩個尺度來調整這個時機:

  • 快取中的物件總數.
  • 將物件加入快取時,為其指定開銷值。

對於開銷值,只有在能很快計算出開銷值的情況下,才應該考慮採用這個尺度,不然反而會加大系統的開銷。

下面我們來看一下快取的用法:快取網路下載的資料

// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;

@end

// Class that uses the network fetcher and caches results
@interface EOCClass : NSObject
@end

@implementation EOCClass {
     NSCache *_cache;
}

- (id)init {

     if ((self = [super init])) {
    _cache = [NSCache new];

     // Cache a maximum of 100 URLs
    _cache.countLimit = 100;


     /**
     * The size in bytes of data is used as the cost,
     * so this sets a cost limit of 5MB.
     */
    _cache.totalCostLimit = 5 * 1024 * 1024;
    }
 return self;
}



- (void)downloadDataForURL:(NSURL*)url { 

     NSData *cachedData = [_cache objectForKey:url];

     if (cachedData) {

         // Cache hit:存在快取,讀取
        [self useData:cachedData];

    } else {

         // Cache miss:沒有快取,下載
         EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];      

        [fetcher startWithCompletionHandler:^(NSData *data){
         [_cache setObject:data forKey:url cost:data.length];    
        [self useData:data];
        }];
    }
}
@end

複製程式碼

在這裡,我們使用URL作為快取的key,將總物件數目設定為100,將開銷值設定為5MB。

NSPurgeableData

NSPurgeableData是NSMutableData的子類,把它和NSCache配合使用效果很好。

因為當系統資源緊張時,可以把儲存NSPurgeableData的那塊記憶體釋放掉。

如果需要訪問某個NSPurgeableData物件,可以呼叫beginContentAccess方發,告訴它現在還不應該丟棄自己所佔據的記憶體。

在使用完之後,呼叫endContentAccess方法,告訴系統在必要時可以丟棄自己所佔據的記憶體。

上面這兩個方法類似於“引用計數”遞增遞減的操作,也就是說,只有當“引用計數”為0的時候,才可以在將來刪去它所佔的記憶體。


- (void)downloadDataForURL:(NSURL*)url { 

      NSPurgeableData *cachedData = [_cache objectForKey:url];

      if (cachedData) {         

            // 如果存在快取,需要呼叫beginContentAccess方法
            [cacheData beginContentAccess];

             // Use the cached data
            [self useData:cachedData];

             // 使用後,呼叫endContentAccess
            [cacheData endContentAccess];


        } else {

                 //沒有快取
                 EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];    

                  [fetcher startWithCompletionHandler:^(NSData *data){

                         NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
                         [_cache setObject:purgeableData forKey:url cost:purgeableData.length];

                          // Don't need to beginContentAccess as it begins            
                          // with access already marked
                           // Use the retrieved data
                            [self useData:data];

                             // Mark that the data may be purged now
                            [purgeableData endContentAccess];

            }];
      }
}
複製程式碼

注意:

在我們可以直接拿到purgeableData的情況下需要執行beginContentAccess方法。然而,在建立purgeableData的情況下,是不需要執行beginContentAccess,因為在建立了purgeableData之後,其引用計數會自動+1;

第51條: 精簡initialize 與 load的實現程式碼

load方法

+(void)load;
複製程式碼

每個類和分類在加入執行期系統時,都會呼叫load方法,而且僅僅呼叫一次,可能有些小夥伴習慣在這裡呼叫一些方法,但是作者建議儘量不要在這個方法裡呼叫其他方法,尤其是使用其他的類。原因是每個類載入程式庫的時機是不同的,如果該類呼叫了還未載入程式庫的類,就會很危險。

initialize方法

+(void)initialize;
複製程式碼

這個方法與load方法類似,區別是這個方法會在程式首次呼叫這個類的時候呼叫(惰性呼叫),而且只呼叫一次(絕對不能主動使用程式碼呼叫)。

值得注意的一點是,如果子類沒有實現它,它的超類卻實現了,那麼就會執行超類的程式碼:這個情況往往很容易讓人忽視。

看一下? :

#import <Foundation/Foundation.h>

@interface EOCBaseClass : NSObject
@end

@implementation EOCBaseClass
+ (void)initialize {
 NSLog(@"%@ initialize", self);
}
@end

@interface EOCSubClass : EOCBaseClass
@end

@implementation EOCSubClass
@end
複製程式碼

當使用EOCSubClass類時,控制檯會輸出兩次列印方法:

EOCBaseClass initialize
EOCSubClass initialize
複製程式碼

因為子類EOCSubClass並沒有覆寫initialize方法,那麼自然會呼叫其父類EOCBaseClass的方法。 解決方案是通過檢測類的型別的方法:

+ (void)initialize {
   if (self == [EOCBaseClass class]) {
       NSLog(@"%@ initialized", self);
    }
}
複製程式碼

這樣一來,EOCBaseClass的子類EOCSubClass就無法再呼叫initialize方法了。 我們可以察覺到,如果在這個方法裡執行過多的操作的話,會使得程式難以維護,也可能引起其他的bug。因此,在initialize方法裡,最好只是設定內部的資料,不要呼叫其他的方法,因為將來可能會給這些方法新增其它的功能,那麼會可能會引起難以排查的bug。

第52條: 別忘了NSTimer會保留其目標物件

在使用NSTimer的時候,NSTimer會生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那麼就會生成保留環。

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end


@implementation EOCClass {
     NSTimer *_pollTimer;
}


- (id)init {
     return [super init];
}


- (void)dealloc {
    [_pollTimer invalidate];
}


- (void)stopPolling {

    [_pollTimer invalidate];
    _pollTimer = nil;
}


- (void)startPolling {
   _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                 target:self
                                               selector:@selector(p_doPoll)
                                               userInfo:nil
                                                repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

複製程式碼

在這裡,在EOCClass和_pollTimer之間形成了保留環,如果不主動呼叫stopPolling方法就無法打破這個保留環。像這種通過主動呼叫方法來打破保留環的設計顯然是不好的。

而且,如果通過回收該類的方法來打破此保留環也是行不通的,因為會將該類和NSTimer孤立出來,形成“孤島”:

孤立了類和它的NSTimer

這可能是一個極其危險的情況,因為NSTimer沒有消失,它還有可能持續執行一些任務,不斷消耗系統資源。而且,如果任務涉及到下載,那麼可能會更糟。。

那麼如何解決呢? 通過“塊”來解決!

通過給NSTimer增加一個分類就可以解決:

#import <Foundation/Foundation.h>

@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();
        }
}
@end

複製程式碼

我們在NSTimer類裡新增了方法,我們來看一下如何使用它:

- (void)startPolling {

         __weak EOCClass *weakSelf = self;    
         _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

               EOCClass *strongSelf = weakSelf;
               [strongSelf p_doPoll];
          }

                                                          repeats:YES];
}

複製程式碼

在這裡,建立了一個self的弱引用,然後讓塊捕獲了這個self變數,讓其在執行期間存活。

一旦外界指向EOC類的最後一個引用消失,該類就會被釋放,被釋放的同時,也會向NSTimer傳送invalidate訊息(因為在該類的dealloc方法中向NSTimer傳送了invalidate訊息)。

而且,即使在dealloc方法裡沒有傳送invalidate訊息,因為塊裡的weakSelf會變成nil,所以NSTimer同樣會失效。

最後的話

總的來說這一部分還是比較容易理解的,更多的只是教我們一些編寫OC程式的規範,並沒有深入講解技術細節。

而三部曲的最後一篇:技巧篇則著重講解了一些在編寫OC程式碼的過程中可以使用的一些技巧。廣義上來講,這些技巧也可以被稱為“規範”,例如“提供全能初始化方法”這一節,但是這些知識點更像是一些“設計模式”目的更偏向於在於解決一些實際問題,因此將這些知識點歸類為“技巧類”。

因為第三篇的內容稍微難一點,所以筆者打算再好好消化幾天,將第三篇的初稿再三潤飾之後呈獻給大家~

本文已同步到個人部落格:傳送門

其他兩篇的傳送門:

《Effective Objective-C 》乾貨三部曲(一):概念篇

《Effective Objective-C 》乾貨三部曲(三):技巧篇

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章