一篇文章拿下《Effective Objective C 2 0編寫高質量iOS與OS X程式碼的52個有效方法》

SuperMairo發表於2019-03-01

最近在重溫這本OC經典之作《Effective Objective-C 2.0編寫高質量iOS與OS X程式碼的52個有效方法》,這篇文章算是重溫之後的產物吧,讀完這篇文章你將快速讀完這本書,由於個人能力有限,難免有一些遺漏或者錯誤,請各位看官不吝賜教!謝謝!同時如果有任何問題也可以在下方留言,歡迎一起交流進步!另外由於篇幅原因,書中一些基礎知識的介紹文中就省略掉了。

目錄

上面就是這本書的目錄,可以點選這裡下載PDF版,原版英文版PDF我也有存~


第一章:熟悉Objective-C

第一條:瞭解Objective-C語言的起源

  1. Objective-C從Smalltalk語言是從Smalltalk語言演化而來,
    Smalltalk是訊息語言的鼻祖。
  2. Objective-CC語言的超集,在C語言基礎上新增了物件導向等特性,可能一開始接觸時你會覺得語法有點奇怪,那是因為Objective-C使用了動態繫結的訊息結構,而JavaC++等等語言使用的是函式呼叫。
  3. 訊息結構函式呼叫的關鍵區別在於:函式呼叫的語言,在編譯階段由編譯器生成一些虛方法表,在執行時從這個表找到所要執行的方法去執行。而使用了動態繫結的訊息結構在執行時接到一條訊息,接下來要執行什麼程式碼是執行期決定的,而不是編譯器。

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

  1. 如果需要引用一個類檔案時,只是需要使用類名,不需要知道其中細節,可以用@class xx.h,這樣做的好處會減少一定的編譯時間。如果是用的#import全部匯入的話,會出現a.h import了b.h,當c.h 又import a.h時,把b.h也都匯入了,如果只是用到類名,真的比較浪費,也不夠優雅
  2. 有時候無法使用@class向前宣告,比如某個類要遵循一項協議,這個協議在另外一個類中宣告的,可以將協議這部分單獨放在一個標頭檔案,或者放在分類當中,以降低引用成本。

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

1.多使用字面量語法來建立字串,陣列,字典等。
傳統建立陣列方法:

NSArray *languages = [NSArray arrayWithObjects:@"PHP", @"Objective-C", someObject, @"Swift", @"Python", nil];
NSString *Swift = [languages objectAtIndex:2];
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"key", @"value", nil];
NSString *value = [languages objectForKey:@"key"];
複製程式碼

字面量:

NSArray *languages = @[@"PHP", @"Objective-C", someObject, @"Swift", @"Python"];
NSString *Swift = languages[2];
NSDictionary *dict = @{@"key" : @"value"};
NSString *value = languages[@"key"];

複製程式碼

這樣做的好處:使程式碼更簡潔,易讀,也會避免nil問題。比如languages資料中 someObject 如果為nil時,字面量語法就會丟擲異常,而使用傳統方法建立的languages陣列值確是@[@"PHP", @"Objective-C"];因為字面量語法其實是一種語法糖,效果是先建立了一個陣列,然後再把括號中的物件都加到陣列中來。
不過字面量語法有一個小缺點就是建立的陣列,字串等等物件都是不可變的,如果想要可變的物件需要自己多執行一步mutableCopy,例如

NSMutableArray *languages = [@[@"PHP", @"Objective-C", @"Swift", @"Python"] mutableCopy];
複製程式碼

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

第4條第5條看這裡

第五條:多用列舉表示狀態、選項、狀態碼

第4條第5條看這裡


第二章:物件、訊息、執行期

第六條:理解“屬性”這一概念

這一條講的是屬性的基本概念,以及屬性的各種修飾符,這些就不多囉嗦了,這裡強調一下:

  1. 定義對外開放的屬性時候儘量做到暴露許可權最小化,不希望被修改的屬性要加上readonly
  2. atomic 並不能保證多執行緒安全,例如一個執行緒連續多次讀取某個屬性的值,而同時還有別的執行緒在修改這個屬性值得時候,也還是一樣會讀到不同的值。atomic 的原理只是在 setter and getter 方法中加了一個@synchronized(self),所以iOS開發中屬性都要宣告為nonatomic,因為atomic嚴重影響了效能,但是在Mac OSX上開發卻通常不存在這個效能問題
  3. 說一下下面的哪個屬性宣告有問題
@property (nonatomic, strong) NSArray *arrayOfStrong;
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
@property (nonatomic, copy) NSMutableArray *mutableArrayOfCopy;
複製程式碼

具體執行示例點選檢視
答案是正常應該這樣宣告

@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
複製程式碼

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

  1. 在類內讀取屬性的資料時,應該通過直接例項變數來讀,這樣不經過Objecit-C的方法派發,編譯器編譯後的程式碼結果是直接訪問存例項變數的那塊記憶體中的值,而不會生成走方法派發的程式碼,這樣的速度會更快。
  2. 給屬性寫入資料時,應該通過屬性的方式來寫入,這樣會呼叫setter 方法。但是在某種情況下初始化方法以及dealloc方法中,總是應該直接通過例項變數來讀寫資料,這樣做是為了避免子類複寫了setter方法造成的異常。
  3. 使用了懶載入的屬性,應該一直保持用屬性的方式來讀取寫入資料。

第八條:理解“物件等同性”這一概念

思考下面輸出什麼?

    NSString *aString = @"iphone 8";
    NSString *bString = [NSString stringWithFormat:@"iphone %i", 8];
    NSLog(@"%d", [aString isEqual:bString]);
    NSLog(@"%d", [aString isEqualToString:bString]);
    NSLog(@"%d", aString == bString);
複製程式碼

答案是110
==操作符只是比較了兩個指標,而不是指標所指的物件

第九條:以“類族模式”隱藏實現細節

為什麼下面這段if 永遠為false

    id maybeAnArray = @[];
    if ([maybeAnArray class] == [NSArray class]) {
         //Code will never be executed
    }
複製程式碼

因為[maybeAnArray class] 的返回永遠不會是NSArray,NSArray是一個類族,返回的值一直都是NSArray的實體子類。大部分collection類都是某個類族中的’抽象基類’
所以上面的if想要有機會執行的話要改成

id maybeAnArray = @[];
    if ([maybeAnArray isKindOfClass [NSArray class]) {
         //Code probably be executed
    }
複製程式碼

這樣判斷的意思是,maybeAnArray這個物件是否是NSArray類族中的一員
** 使用類族的好處:可以把實現細節隱藏再一套簡單的公共介面後面 **

第十條:在既有類中使用關聯物件存放自定義資料

這條講的是objc_setAssociatedObjectobjc_getAssociatedObject,如何使用在這裡就不多說了。值得強調的一點是,用關聯物件可能會引入難於查詢的bug,畢竟是在runtime階段,所以可能要看情況謹慎選擇

第十一條:理解“objc_msgSend”的作用

之前在瞭解Objective-C語言的起源有提到過,Objective-C是用的訊息結構。這條就是讓你理解一下怎麼傳遞的訊息。

  1. 在Objective-C中,如果向某個物件傳遞訊息,那就會在執行時使用動態繫結(dynamic binding)機制來決定需要呼叫的方法。但是到了底層具體實現,卻是普通的C語言函式實現的。這個實現的函式就是objc_msgSend,該函式定義如下:
void objc_msgSend(id self, SEL cmd, ...) 
複製程式碼

這是一個引數個數可變的函式,第一引數代表接收者,第二個引數代表選擇子(OC函式名),後續的引數就是訊息(OC函式呼叫)中的那些引數
2. 舉例來說:

id return = [git commit:parameter];
複製程式碼

上面的Objective-C方法在執行時會轉換成如下函式:

id return = objc_msgSend(git, @selector(commit), parameter);
複製程式碼

objc_msgSend函式會在接收者所屬的類中搜尋其方法列表,如果能找到這個跟選擇子名稱相同的方法,就跳轉到其實現程式碼,往下執行。若是當前類沒找到,那就沿著繼承體系繼續向上查詢,等找到合適方法之後再跳轉 ,如果最終還是找不到,那就進入訊息轉發的流程去進行處理了。
3. 說過了OC的函式呼叫實現,你會覺得訊息轉發要處理很多,尤其是在搜尋上,幸運的是objc_msgSend在搜尋這塊是有做快取的,每個OC的類都有一塊這樣的快取,objc_msgSend會將匹配結果快取在快速對映表(fast map)中,這樣以來這個類一些頻繁呼叫的方法會出現在fast map 中,不用再去一遍一遍的在方法列表中搜尋了。
4. 還有一個有趣的點,就是在底層處理髮送訊息的時候,有用到尾呼叫優化,大概原理就是在函式末尾呼叫某個不含返回值函式時,編譯器會自動的不在棧空間上重新進行分配記憶體,而是直接釋放所有呼叫函式內部的區域性變數,然後直接進入被呼叫函式的地址。

第十二條:理解訊息轉發機制

關於這條這看看這篇文章:iOS理解Objective-C中訊息轉發機制附Demo

第十三條:用“方法調配技術”除錯“黑盒方法”

這條講的主要內容就是 Method Swizzling,通過執行時的一些操作可以用另外一份實現來替換掉原有的方法實現,往往被應用在向原有實現中新增新功能,比如擴充套件UIViewController,在viewDidLoad裡面增加列印資訊等。具體例子可以點選我檢視

第十四條:理解“類物件”的用意

Objective-C類是由Class型別來表示的,它實際上是一個指向objc_class結構體的指標。它的定義如下:

typedef struct objc_class *Class;
複製程式碼

在<objc/runtime.h>中能看到他的實現:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;    ///< 指向metaClass(元類)

#if !__OBJC2__
        Class super_class                       OBJC2_UNAVAILABLE;  ///< 父類
        const char *name                        OBJC2_UNAVAILABLE;  ///< 類名
        long version                            OBJC2_UNAVAILABLE;  ///< 類的版本資訊,預設為0
        long info                               OBJC2_UNAVAILABLE;  ///< 類資訊,供執行期使用的一些位標識
        long instance_size                      OBJC2_UNAVAILABLE;  ///< 該類的例項變數大小
        struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  ///< 該類的成員變數連結串列
        struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  ///< 方法定義的連結串列
        struct objc_cache *cache                OBJC2_UNAVAILABLE;  ///< 方法快取
        struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  ///< 協議連結串列
#endif

} OBJC2_UNAVAILABLE;
複製程式碼

此結構體存放的是類的“後設資料”(metadata),例如類的例項實現了幾個方法,具備多少例項變數等資訊。
這裡的isa指標指向的是另外一個類叫做元類(metaClass)。那什麼是元類呢?元類是類物件的類。也可以換一種容易理解的說法:

  1. 當你給物件傳送訊息時,runtime處理時是在這個物件的類的方法列表中尋找
  2. 當你給類發訊息時,runtime處理時是在這個類的元類的方法列表中尋找

我們來看一個很經典的圖來加深理解:

一篇文章拿下《Effective Objective C 2 0編寫高質量iOS與OS X程式碼的52個有效方法》

可以總結為下:

  1. 每一個Class都有一個isa指標指向一個唯一的Meta Class
  2. 每一個Meta Classisa指標都指向最上層的Meta Class,這個Meta ClassNSObject的Meta Class。(包括NSObject的Meta Classisa指標也是指向的NSObject的Meta Class,也就是自己,這裡形成了個閉環)
  3. 每一個Meta Classsuper class指標指向它原本ClassSuper Class的Meta Class (這裡最上層的NSObject的Meta Classsuper class指標還是指向自己)
  4. 最上層的NSObject Class的super class指向 nil

第三章:介面與API設計

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

Objective-C沒有類似其他語言那樣的名稱空間機制(namespace),比如說PHP中的

<?php
namespace RootSubsubnamespace;
複製程式碼

這就會導致當你不小心實現了兩個相同名字的類,或者把兩個相對獨立的庫匯入專案時而他們又恰好有重名的類的時候該類所對應的符號和Meta Class符號定義了兩次。所以很容易產生這種命名衝突,讓程式的連結過程中出現出現重複的符號造成報錯。
為了避免這種情況,我們要儘量在類名,以及分類和分類方法上增加字首,還有一些巨集定義等等根據自己專案來定吧

第十六條:提供“全能初始化方法”

如果建立類的例項的方式不止一種,那麼這個類就會有多個初始化方法,這樣做很好,不過還是要在其中選定一個方法作為全能初始化方法,剩下的其餘的初始化方法都要呼叫它,這樣做的好處是以後如果初始化的邏輯更改了只需更改一處即可,或者是交給子類覆寫的時候也只覆寫這一個方法即可~
舉個例子來說:可以看一下NSDate的實現在NSDate.h中NSDate類中定義了一個全能初始化方法:

- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
複製程式碼

其餘的類似初始化方式定義在NSDate (NSDateCreation) 分類中

- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
複製程式碼

在NSDate文件中有一條:If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must do these things:然後其中要做的有一步就是

Override [initWithTimeIntervalSinceReferenceDate:
](apple-reference-documentation://hcslylvSCo), one of the designated initializer methods`
複製程式碼

這個是我們組織程式碼過程中應該學習的地方!

第十七條:實現description方法

這條講的是可以通過覆寫description方法或者debugDescription方法來在NSLog列印時或者LLDB列印時輸出更多的自定義資訊。(資料和字典的可以通過覆寫descriptionWithLocale:方法)
友情提示:不要在description中使用 NSLog("%@",self);,不然會掉進無底深淵啊
這裡我有一個有趣的想法,不過還沒完全實現,就是想通過覆寫description能把任何一個物件的屬性值名稱,屬性值都一一完整的記錄下來,可以點選檢視

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

這條主要講儘量使用不可變的物件,也就是在對外屬性宣告的時候要儘量加上readonly修飾,預設是readwrite,這樣一來,在外部就只能讀取該資料,而不能修改它,使得這個類的例項所持有的資料更加安全。如果外部想要修改,可以提供方法來進行修改。
不要把可變的collection作為屬性公開,而應提供相關方法,以此修改物件中的可變collection(這條個人感覺一般在常用、重要的類才有必要,畢竟也增加了不少程式碼量)
比如例子:

//Language.h
@property (nonatomic, strong) NSSet *set;
複製程式碼

應該改為

//Language.h
@property (nonatomic, strong, readonly) NSSet *languages;
- (void)addLanguage:(NSString *)language;
- (void)removeLanguage:(NSString *)language;
//**.m
@implementation Language {
    NSMutableSet *mutableLanguages;
}
- (NSSet *)languages {
    return [_mutableLanguages copy];
}
- (void)addLanguage:(NSString *)language {
    [_mutableLanguages addObject:language];
}
- (void)removeLanguage:(NSString *)language {
     [_mutableLanguages removeObject:language];
}
複製程式碼

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

這條不用太強調了,具體也可以參照一下我之前擬的Objective-C程式設計規範及建議,後續可能會不斷補充更新

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

這條講的是應該為類內的私有方法增加字首,以便區分,這個感覺因人而異吧,感覺只要你不隨便把私有方法暴露在.h檔案都能接受,曾遇到過這樣的同事,感覺其不太適合寫程式吧。

第二十一條:理解Objective-C錯誤模型

很多語言都有異常處理機制,Objective-C也不例外,Objective-C也有類似的@throw,不過在OC中使用@throw可能會導致記憶體洩漏,可能是它被設計的使用場景的問題。建議@throw只用來處理嚴重錯誤,也可以理解為致命錯誤(fatal error),那麼處理一般錯誤的時候(nonfatal error)時可以使用NSError。

第二十二條:理解NSCopying協議

在OC開發中,使用物件時經常需要拷貝它,我們會通過copy/mutbleCopy來完成。如果想讓自己的類支援拷貝,那必須要實現NSCopying協議,只需要實現一個方法:

- (id)copyWithZone:(NSZone*)zone
複製程式碼

當然如果要求返回物件是可變的型別就要用到NSMutableCopying協議,相應方法

- (id)mutableCopyWithZone:(NSZone *)zone
複製程式碼

在拷貝物件時,需要注意拷貝執行的是淺拷貝還是深拷貝。深拷貝在拷貝物件時,會將物件的底層資料也進行了拷貝。淺拷貝是建立了一個新的物件指向要拷貝的內容。一般情況應該儘量執行淺拷貝。


第四章:協議與分類

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

這條講的也比較基礎,就是基本的delegate,protocal使用。
有一點稍微說一下:當某物件需要從另外一個物件中獲取資料時,可以使用委託模式,這種用法經常被稱為“資料來源協議”(Data source Protocal)類似 UITableviewUITableViewDataSource
另外在Swift中有一個很重要的思想就是面向協議程式設計。當然OC中也可以用協議來降低程式碼耦合性,必要的時候也可以替代繼承,因為遵循同一個協議的類可以是任何,不必是同一個繼承體系下。

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

這條主要說的是通過分類機制,可以把類分成很多歌易於管理的小塊。也是有一些前提的吧,可能是這個類業務比較複雜,需要瘦身,需要解耦等等。作者還推薦把私有方法統一放在Private分類中,以隱藏實現細節。這個個人覺得視情況而定吧。

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

向第三方類的分類名稱加上你專用的字首,這點不必多說,?

第二十六條:勿在分類中宣告屬性

不要在分類中宣告屬性,除了“class-continuation”分類中。那什麼是“class-continuation”分類呢,其實就是我們經常在.m檔案中用到的,例如:

//Swift.m 
@interface Swift () 
//這個就是“class-continuation”分類
@end
@implementation Swift
@end
複製程式碼

第二十七條:使用“class-continuation”分類隱藏實現細節

這條跟之前的也有點重複,最終目的還是要儘量在公共介面中向外暴露的內容最小化,隱藏實現細節,只告訴怎麼呼叫,怎麼使用即可。具體實現以及屬性的可修改許可權盡可能的隱藏掉。

第二十八條:通過協議提供匿名物件

  1. 協議可以在某種程度上提供匿名物件,例如id<someProtocal> object。object物件的型別不限,只要能遵從這個協議即可,在這個協議裡面定義了這個物件所應該實現的方法。
  2. 如果具體型別不重要,重要的是物件能否處理好一些特定的方法,那麼就可以使用這種協議匿名物件來完成。

第五章:記憶體管理

第二十九條:理解引用計數

  1. 理解引用計數這個可以通過《Objective-C 高階程式設計》這本書中的例子來理解,比較直觀,大概如下:
一篇文章拿下《Effective Objective C 2 0編寫高質量iOS與OS X程式碼的52個有效方法》
對照明裝置所做的工作 對OC物件所做的動作
開燈 生成物件
需要照明 持有
不需要照明 釋放
關燈 廢棄
記憶體管理的思考方式 對應OC方法
自己生成的物件,自己所持有 alloc/new/copy/mutableCopy等
非自己生成的物件(比如[NSArray array]),自己也能持有 retain
不再需要自己持有的物件時釋放 release
當物件不被任何其他物件持有時廢棄 dealloc
  1. 自動釋放池: 可以看到在我們程式中入口檔案main.m中main函式中就包裹了一層autoreleasepool
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([HSAppDelegate class]));
    }
}
複製程式碼

autoreleasepool可以延長物件的生命期,使其在跨越方法呼叫邊界後依然可以存活一段時間,通常是在下一次“時間迴圈”(event loop)時釋放,不過也可能會執行的早一點。
3. 保留環: 也稱retain cycle,就是迴圈引用。形成原因就是物件之間相互用強引用指向對方,會使得全部都無法得以釋放。解決方案通常是使用弱引用(weak reference)

第三十條:以ARC簡化引用計數

使用ARC,可以省略對於引用計數的操作,所以在ARC下呼叫物件的retain,release,autorelease,dealloc方法時系統會報錯。
這裡要注意CoreFoundation 物件不歸ARC管理,開發中如果有用到還是要誰建立誰釋放,適時呼叫CFRetain/CFRelease。

第三十一條:在delloc方法中只釋放引用並解除監聽

不要在delloc方法中呼叫其他方法,尤其是需要非同步執行某些任務又要回撥的方法,這樣的很危險的行為,很可能非同步執行完回撥的時候該物件已經被銷燬了,這樣就沒得玩了,crash了。
在delloc方法裡應該製作一些釋放相關的事情,包括不限於一些KVO取消訂閱,remove 通知等。

第三十二條:編寫“異常安全程式碼”時留意記憶體管理問題

這條有點重複,之前已經說過了,OC中丟擲異常的時候可能會引起記憶體洩漏,注意一下使用的時機,或者注意在@try捕獲異常中清理乾淨。

第三十三條:以弱引用避免保留環

這條比較簡單,內容主旨就是標題:以弱引用避免保留環(Retain Cycle)

第三十四條:以“@autoreleasepool”降低記憶體峰值

在遍歷處理一些大陣列或者大字典的時候,可以使用自動釋放池來降低記憶體峰值,例如:

NSArray *people = /*一個很大的陣列*/
NSMutableArray *employeesArray = [NSMutableArray new];
for (NSStirng *name in people) {
    @autoreleasepool {
        MLEmployee *employee = [MLEmployee alloc] initWithName:name];
        [employeesArray addObject:employee];
    }
}
複製程式碼

第三十五條:用“殭屍物件”除錯記憶體管理問題

一篇文章拿下《Effective Objective C 2 0編寫高質量iOS與OS X程式碼的52個有效方法》

如上圖,勾選這裡可以開啟殭屍物件設定。開啟之後,系統在回收物件時,不將其真正的回收,而是把它的isa指標指向特殊的殭屍類,變成殭屍物件。殭屍類能夠響應所有的選擇子,響應方式為:列印一條包含訊息內容以及其接收者的訊息,然後終止應用程式

第三十六條:不要使用retainCount

在蘋果引入ARC之後retainCount已經正式廢棄,任何時候都不要呼叫這個retainCount方法來檢視引用計數了,因為這個值實際上已經沒有準確性了。但是在MRC下還是可以正常使用


第六章:Block與GCD

第三十七條:理解block

根據block在記憶體中的位置,block被分成三種型別:

  1. NSGlobalBlock 全域性塊:
    這種塊執行時無需獲取外界任何狀態,塊所使用的記憶體區域在編譯器就可以完全確定,所以該塊宣告在全域性記憶體中。如果全域性塊執行copy會是一個空操作,相當於什麼都沒做。全域性塊例如:
void (^block)() = ^{
    NSLog(@"I am a NSGlobalBlock");
}
複製程式碼
  1. NSStackBlock 棧塊:
    棧塊儲存於棧區,超出變數作用域,棧上的block以及__block變數都會被銷燬。例如:
NSString *name = @"PHP";
void (^block)() = ^{
    NSLog(@"世界上最好的程式語言是%@", name);
};
NSLog(@"%@", block);
複製程式碼

執行下你會發現控制檯列印的是:

<__NSStackBlock__: 0x7fff5480fa18>
複製程式碼

什麼,你說什麼,你列印出來的是__ NSMallocBlock __? 那是因為你在ARC下編譯的,ARC下編譯器編譯時會幫你優化自動幫你加上了copy操作,你可以用-fno-objc-arc關閉ARC再看一下
3. NSMallocBlock 堆塊:
NSMallocBlock內心獨白:我已經被暴露了,為什麼要最後才介紹我!!
堆block記憶體儲存於堆區,在變數作用域結束時不受影響。通過之前在ARC下的輸出已經看到了__ NSMallocBlock __.所以我們在定義block型別的屬性時常常加上copy修飾,這個修飾其實是多餘的,系統在ARC的時候已經幫我們做了copy,但是還是建議寫上copy。

第三十八條:為常用的塊型別建立typedef

這條主要是為了程式碼更易讀,也比較重要。

- (void)getDataWithHost:(NSString *)host success:(void (^)(id responseDic))success;
//以上要改成下面這種
typedef void (^SuccessBlock)(id responseDic);
- (void)getDataWithHost:(NSString *)host success:(SuccessBlock)success;
複製程式碼

第三十九條:用handler塊降低程式碼分散程度

在iOS開發中,我們經常需要非同步執行一些任務,然後等待任務執行結束之後通知相關方法。實現此需求的做法很多,比如說有些人可能會選擇用委託協議。那麼在這種非同步執行一些任務,然後等待執行結束之後呼叫代理的時候,可能程式碼就會比較分散。當多個任務都需要非同步,等等就顯得比較不那麼合理了。
所以我們可以考慮使用block的方式設計,這樣業務相關的程式碼會比較緊湊,不會顯得那麼凌亂。

第四十條:用塊引用其所屬物件是不要出現保留環

這點比較基礎了,但是要稍微說一下,不是一定得在block中使用weakself,比如下面:

[YTKNetwork requestBlock:^(id responsObject) {
      NSLog(@"%@",self.name);
  }];
複製程式碼

block 不是被self所持有的,在block中就可以使用self

第四十一條:多用派發佇列,少用同步鎖

在iOS開發中,如果有多個執行緒要執行同一份程式碼,我們可能需要加鎖來實現某種同步機制。有人可能第一印象想到的就是@synchronized(self),例如:

- (NSString*)someString {
    @synchronized(self) {
        return _someString;
    }
}
- (void)setSomeString:(NSString*)someString {
     @synchronized(self) {
        _someString = someString;
    }
}
複製程式碼

這樣寫法效率很低,而且也不能保證執行緒中覺得的安全。如果有很多屬性,那麼每個屬性的同步塊都要等其他同步塊執行完畢才能執行。
應該用GCD來替換:

_syncQueue = dispatch_queue_create("syncQueue", DISPATCH_QUEUE_CONCURRENT);

//讀取字串
- (NSString*)someString {
    __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
- (void)setSomeString:(NSString*)someString {
     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}
複製程式碼

第四十二條:多用GCD,少用performSelector系列方法

Objective-C本質上是一門分廠動態的語言,開發者在開發中可以指定任何一個方法去呼叫,也可以延遲呼叫一些方法,或者指定執行方法的執行緒。一般我們會想到performSelector,但是在GCD出來之後基本就沒那麼需要performSelector了,performSelector也有很多缺點:

  1. 記憶體管理問題:在ARC下使用performSelector我們經常會看到編譯器發出如下警告:warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]
  2. performSelector的返回值只能是void或物件型別。
  3. performSelector無法處理帶有多個引數的選擇子,最多隻能處理兩個引數。
    為了改變這些,我們可以用下面這種方式
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});
複製程式碼

替換掉

[self performSelectorOnMainThread:@selector(doSomething) 
                       withObject:nil 
                    waitUntilDone:NO];
複製程式碼

然後還可以用

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 
                                (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
    [self doSomething];
});
複製程式碼

替換

[self performSelector:@selector(doSomething) 
           withObject:nil 
           afterDelay:5.0];
複製程式碼

第四十三條:掌握GCD以及操作佇列的使用時機

GCD技術確實很棒,但是也有一些侷限性,或者說有一些場景並不適合。比如過想取消佇列中的某個操作,或者需要後臺執行任務。還有一種技術叫NSOperationQueue,其實NSOperationQueue跟GCD有很多相像之處。NSOperationQueue在GCD之前就已經有了,GCD就是在其某些原理上構建的。GCD是C層次的API,而NSOperation是重量級的Objective-C物件。
使用NSOperationNSOperationQueue的優點:

  1. 支援取消某個操作:在執行任務前,可以在NSOperation物件上呼叫cancel方法,用以表明此任務不需要執行。不過已經啟動的任務無法取消。GCD佇列是無法取消的,GCD是“安排好之後就不管了(fire and forget)”。
  2. 支援指定操作間的依賴關係:一個操作可以依賴其他多個操作,例如從伺服器下載並處理檔案的動作可以用操作來表示,而在處理其他檔案之前必須先下載“清單檔案”。而後續的下載工作,都要依賴於先下載的清單檔案這一操作。這時如果操作佇列允許併發執行的話,後續的下載操作就可以在他依賴的下載清單檔案操作執行完畢之後開始同時執行。
  3. 支援通過KVO監控NSOperation物件的屬性:可以通過isCancelled屬性來判斷任務是否已取消,通過isFinished屬性來判斷任務是否已經完成等等。
  4. 支援指定操作的優先順序:操作的優先順序表示此操作與佇列中其他操作之間的優先關係,優先順序搞的操作先執行,優先順序低的後執行。GCD的佇列也有優先順序,不過不是針對整個佇列的。
  5. 重用NSOperation物件。在開發中你可以使用NSOperation的子類或者自己建立NSOperation物件來儲存一些資訊,可以在類中定義方法,使得程式碼能夠多次使用。不必重複自己。

第四十四條:通過Dispatch Group機制,根據系統資源狀況來執行任務

這條主要是介紹dispatch group,任務分組的功能。他可以把任務分組,然後等待這組任務執行完畢時會有通知,開發者可以拿到結果然後繼續下一步操作。
另外通過dispatch group在併發佇列上同時執行多項任務的時候,GCD會根據系統資源狀態來幫忙排程這些併發執行的任務。

第四十五條:使用dispatch_once來執行只需要執行一次的執行緒安全程式碼

這條講的是常用的dispatch_once

+ (id)sharedInstance {
     static EOCClass *sharedInstance = nil;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
    });
     return sharedInstance;
}
複製程式碼

dispatch_once比較高效,沒有重量級的同步機制。

第四十六條:不要使用dispatch_get_current_queue

  1. dispatch_get_current_queue 函式的行為常常與開發者所預期的不同,此函式已經廢棄,只應做除錯之用。
  2. 由於GCD是按層級來組織的,所以無法單用某個佇列物件來描述”當前佇列”這一概念。
  3. dispatch_get_current_queue 函式用於解決由不可以重入的程式碼所引發的死鎖,然後能用此函式解決的問題,通常也可以用”佇列特定資料”來解決。

第七章:系統框架

第四十七條:熟悉系統框架

在Objective-C中除了Foundation 與CoreFoundation之外還有很多系統庫,其中包括但不限於下面列出的這些:

  1. CFNetwork:此框架提供了C語言級別的網路通訊能力,它將BSD socket抽象成了易於使用的網路介面。而Foundation則將該框架裡的部分內容封裝為Objective-C介面,以便進行網路通訊。
  2. CoreAudio:此框架所提供的C語言API可以用來操作裝置上的音訊硬體。
  3. AVFoundation:此框架所提供的Objective-C物件可用來回訪並錄製音訊及視訊,比如能夠在UI檢視類裡播放視訊。
  4. CoreData:此框架所提供的Objective-C介面可以將物件放入資料庫,將資料持久化。
  5. CoreText:此框架提供的C語言介面可以高效執行文字排版以及渲染操作。
  6. SpriteKit :遊戲框架
  7. CoreLocation、MapKit :定位地圖相關框架
  8. Address Book框架:需要使用通訊錄時才使用該框架
  9. Music Libraries框架:音樂庫相關框架
  10. HealthKit框架:健康相關框架
  11. HomeKit框架:為智慧化硬體提供的框架
  12. CloudKit : iCloud相關的框架
  13. Passbook、PassKit框架:為了在應用中使用者可以很容易的訪問他們之前購買的活動門票、旅行車票、優惠券等等提供的框架

第四十八條:多用塊列舉,少用for迴圈

  1. 遍歷collection中的元素有四種方式,最基本的辦法就是for迴圈,其次是NSEnumerator遍歷法,還有快速遍歷法(for in),以及塊列舉法。塊列舉是最新,最先進的方式。
  2. 塊列舉法是通過GCD來併發執行遍歷操作
  3. 若提前知道待遍歷的collection含有何種物件,則應修改塊簽名,指出物件的具體型別。

第四十九條:對自定義其記憶體管理語義的collecion使用無縫橋接

通過無縫橋接技術,可以在定義於Foundation框架中的類和CoreFoundation框架中的C語言資料結構之間來回轉換。
下面程式碼展示了簡單的無縫橋接:

NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
//Output: Size of array = 5
複製程式碼

轉換操作中的__bridge告訴ARC如何傳力轉換所涉及的OC物件,也就是ARC仍然具備這個OC物件的所有權。__bridge_retained與之相反。這裡要注意用完了陣列要自己釋放,使用CFRelease(aCFArray)前面有提到過的。

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

在構建快取時應該儘量選用NSCache而非NSDictionary,NSCache會在系統資源將要耗盡時自動刪減快取,而使用NSDictionary只能通過系統低記憶體警告方法去手動處理。此外NSCache還會看情況刪減最久未使用的物件,而且是執行緒安全的。

第五十一條:精簡initialize與load的實現程式碼

  1. load與initialize 方法都應該實現的精簡一點,這樣有助於保持應用程式的響應能力,也可以減少引入依賴環的機率
  2. 無法在編譯器設定的全域性常量,可以放在initialize方法裡面初始化。
    另外沒搞清楚load 與 initialize的可以看這裡, 我之前有出過一道有點腦殘有點繞的題(別拍磚,?),可以點選這裡檢視

第五十二條:別忘了NSTimer會保留其目標物件

在iOS開發中經常會用到定時器: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
複製程式碼

如果建立了本類的例項,並呼叫其startPolling方法開始定時器,由於目標物件是self,所以要保留此例項,因為定時器是用成員變數存放的,所以self也保留了計時器,所以此時存在保留環。此時要麼呼叫stopPolling,要麼令系統將此例項回收,只有這樣才能打破保留環。
這是一個很常見的記憶體洩漏,那麼怎麼解決呢?這個問題可以通過block來解決。可以新增這樣的一個分類:

#import <Foundation/Foundation.h>
//.h
@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                         repeats:(BOOL)repeats;
@end
//.m
@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
複製程式碼

EOF : 由於個人能力有限,難免有一些遺漏或者錯誤,請各位看官不吝賜教!謝謝!同時如果有任何問題也可以在下方留言,歡迎一起交流進步~最後感謝作者Matt Galloway以及譯者!更多細節還是請翻閱圖書,可以點選這裡下載PDF版,原版英文版PDF我也有存~本文已經同步到個人部落格

相關文章