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

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

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

而後用思惟導圖整理了一下: 程序員

三部曲分佈圖

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

第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 的取下標操做也應該儘可能使用字面量語法。bash

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 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

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

公衆號:程序員維他命
相關文章
相關標籤/搜索