Effective Objective-C 2.0 敲門磚

Effective Objective-C 2.0 編寫高質量iOS和OS X代碼的52個有效方法

前言

這本書和Objective-C高級編程-iOS和OS X多線程和內存管理實在是iOS開發人員必讀書. 實在是太經典了. 相信懂的人自然懂~

Objective-C高級編程的讀書筆記我已經整理好發布了, 大家感興趣的話可以去看看, 不感興趣就直接略過吧~
Objective-C高級編程讀書筆記之內存管理
Objective-C高級編程讀書筆記之blocks
Objective-C高級編程讀書筆記之GCD

這篇文章只是一個敲門磚, 大家不要指望看了這篇文章就不用去看書了, 那是不可能的, 也是遠遠不夠的, 只是希望各位能借助我這篇文章, 留個整體的印象, 然后再帶著問題去研讀這本書. 那才能達到最好的效果.


目錄

第1章 : 熟悉Objective-C
第2章 : 對象, 消息, 運行時
第3章 : 接口與API設計
第4章 : 協議和分類
第5章 : 內存管理
第6章 : 塊與大中樞派發(也就是Block與GCD)
第7章 : 系統框架


第1章 : 熟悉Objective-C

1. Objective-C是一門動態語言, 該語言使用的是"消息結構"而非"函數調用".
  • 消息結構 : 運行時所執行的代碼由運行時環境決定
  • 函數調用 : 運行時所執行的代碼由編譯期決定.

也就是說[person run];

給person對象發送一條run消息 : 不到程序運行的時候你都不知道他究竟會執行什么代碼. 而且, person這個對象究竟是Person類的對象, 還是其他類的對象, 也要到運行時才能確定, 這個過程叫動態綁定.

2. 堆空間

對象所占內存總是分配在堆空間中. 不能在棧中分配Objective-C對象.

  • 棧空間 : 棧空間的內存不用程序員管理.
  • 堆空間 : 堆空間的內存需要程序員管理.
NSString *anString = @"Jerry";
NSString *anotherString = anString;

以上代碼的意思是, 在堆空間中創建一個NSString實例對象, 然而棧空間中分配兩個指針分別指向該實例. 如圖,


堆和棧
在類的頭文件中盡量少引入其他文件

在類的頭文件中用到某個類, 如果沒有涉及到其類的細節, 盡量用@class向前聲明該類(等于告訴編譯器這是一個類, 其他你先別管)而不導入該類的頭文件以避免循環引用和減少編譯時間.

多用字面量語法, 少用與之等價的方法

我們知道, 現在我們創建Foundation框架的類時有許多便捷的方法, 如

NSString *string = @"Jerry";
NSNumber *number = @10;
NSArray *array = @[obj, obj1, obj2];
NSDictionary *dict = @{
                     @"key1" : obj1,
                     @"key2" : obj2,
                     @"key3" : obj3 };

我用們字面量語法替代傳統的alloc-init來創建對象的好處 :

  • 方便直觀
  • 更加安全
  • 更利于debug

局限性 :

  • 只有NSString, NSArray, NSDictionary, NSNumber支持字面量語法
  • 若想用字面量語法創建出可變對象, 則需要再次調用mutableCopy方法復制多一份(多調用了一個方法, 多創建了一個對象. 不必要)

關于字面量語法, 有位哥們寫得很通俗易懂, 可以去移步到淺談OC字面量語法這里看看.

多用類型常量, 少用#define預處理指令

為什么少用#define預處理指令?

  • 用預處理指令定義的常量不含類型信息
  • 編譯時只會進行簡單查找與替代操作, 會分配多次內存
  • 如果有人重新定義了常量值, 則會導致程序中常量值不一致

為什么多用類型常量?

  • 在實現文件中使用static const定義只在該文件內可見的常量, 其他文件無法使用(無需給常量名稱加前綴)
  • 在頭文件中使用extern來聲明全局常量, 并在實現文件中定義其值, 可以供整個程序使用(需要給常量名稱加前綴)

針對const#define的優劣, 可參考我之前寫過的一篇文章15分鐘弄懂 const 和 #define

用枚舉來表示狀態, 選項, 狀態碼

相對于魔法數字(Magic Number), 使用枚舉的好處不言而喻. 這里只說兩個.

  1. 如果枚舉類型的多個選項不需要組合使用, 則用NS_ENUM
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
     UIViewAnimationTransitionNone,
     UIViewAnimationTransitionFlipFromLeft,
     UIViewAnimationTransitionFlipFromRight,
     UIViewAnimationTransitionCurlUp,
     UIViewAnimationTransitionCurlDown,
};
  1. 如果枚舉類型的多個選項可能組合使用, 則用NS_OPTIONS
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
       UIViewAutoresizingNone                 = 0,
       UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
       UIViewAutoresizingFlexibleWidth        = 1 << 1,
       UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
       UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
       UIViewAutoresizingFlexibleHeight       = 1 << 4,
       UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

以上代碼為蘋果源碼.
使用NS_ENUM和NS_OPTIONS來替代C語言的enum的好處

  • 可以自定義枚舉的底層數據類型
  • 在C中使用C的語法, 在OC中使用OC的語法, 保持語法的統一

另外, 在處理枚舉的switch語句中, 不要使用default分支, 因為以后你加入新枚舉之后, 編譯器會提示開發者 : switch語句沒有處理所有枚舉(沒使用default的情況下).


第2章 : 對象, 消息, 運行時

上一章我們說到, Objective-C是一門動態語言, 其動態性就由這一章來說明.

理解"屬性"這一概念
@interface Person : NSObject {
@public
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_address;
}

編寫過Java或C++的人應該比較熟悉這種寫法, 但是這種寫法問題很大!!!
對象布局在編譯器就已經固定了. 只要碰到訪問_firstName變量的代碼, 編譯器就把其替換為"偏移量", 這個偏移量是"硬編碼", 表示該變量距離存放對象的內存區域的起始地址有多遠.

目前這樣看沒有問題, 但是只要在_firstName前面再加一個實例變量就能說明問題了.

@interface Person : NSObject {
@public
    NSDate *_birthday;
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_address;
}

原來表示_firstName的偏移量現在卻指向_birthday了. 如圖

在類中新增另一個實例變量前后的數據布局圖

有人可能會有疑問, 新增實例變量不是要寫代碼然后編譯運行程序嗎? 重新編譯后對象布局不就又變正確了嗎? 錯誤! 正是因為Objective-C是動態語言, 他可以在運行時動態添加實例變量, 那時對象布局早就已固定不能再更改了.

那么Objective-C是怎么避免這種情況的呢? 它把實例變量當做一種存儲偏移量所用的"特殊變量", 交由"類對象"保管(類對象將會在本章后面說明). 此時, 偏移量會在運行時進行查找, 如果類的定義變了, 那么存儲的偏移量也會改變, 這樣在運行時無論何時訪問實例變量, 都能使用正確的偏移量. 有了這種穩固的ABI(Application Binary Interface), OC就能在運行時給類動態添加實例變量而不會發生訪問錯誤了.

@property, @synthesize, @dynamic

這是本節的重中之重. 我們必須要搞清楚使用@property, @synthesize, @dynamic關鍵字, 編譯器會幫我們做了什么, 才能更好地掌握使用屬性.

  • @property
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

以上代碼編譯器會幫我們分解成setter和getter方法聲明, 以上代碼與以下代碼等效

@interface Person : NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;
@end
  • @synthesize
@implementation Person
@synthesize firstName;
@end

以上代碼相當于給Person類添加一個_firstName的實例變量并為該實例變量生成setter和getter方法的實現(存取方法).

可以利用@synthesize給實例變量取名字(默認為_xxx, 例如@property聲明的是name, 則生成的是_name的實例變量)

@implementation Person
@synthesize firstName = myFirstName;
@end

以上代碼就是生成myFirstName的實例變量了. 由于OC的命名規范, 不推薦這么做. 沒必要給實例變量取另一個名字.

  • @dynamic
@implementation Person
@dynamic firstName;
@end

該代碼會告訴編譯器 : 不要自動創建實現屬性(property)所用的實例變量(_property)和存取方法實現(setter和getter).

也就是說, 實例變量不存在了, 因為編譯器不會自動幫你創建了. 而且如果你不手動實現setter和getter, 使用者用點語法或者對象方法調用setter和getter時, 程序會直接崩潰, 崩潰原因很簡單 : unrecognized selector sent to instance

上代碼

// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
-------------------------------------------
// Person.m
@implementation Person
@dynamic name;
@end
-------------------------------------------
// main.m
int main(int argc, const char * argv[]) {
    Person *p = [[Person alloc] init];
    p.name = @"Jerry";
    return 0;
}
-------------------------------------------
// 程序崩潰, 控制臺輸出
-[Person setName:]: unrecognized selector sent to instance 

原因很簡單, 我用@dynamic騙編譯器, 你不用幫我生成實例變量跟方法實現啦, 我自己來. 結果運行的時候卻發現你丫的根本找不到實現方法, 所以崩潰了唄~

總結下

在現在的編譯器下,

  1. @property會為屬性生成setter和getter的方法聲明, 同時調用@synthesize ivar = _ivar生成_ivar實例變量和存取方法的實現
  2. 手動調用@synthesize可以用來修改實例變量的名稱
  3. 手動調用@dynamic可以告訴編譯器: 不要自動創建實現屬性所用的實例變量, 也不要為其創建實例變量的存取方法聲明與實現.
readonly與readwrite

以上文檔說明, 就算你沒有用@dynamic, 只要你手動實現了setter和getter方法(屬性為readwrite情況下)或者手動實現getter方法(屬性為readonly情況下), @property關鍵字也不會自動調用@synthesize來幫你合成實例變量了.

以上特性均可以使用runtime打印類的實例變量列表來印證.

在對象內部盡量直接訪問實例變量

為什么呢? 使用點語法不好嗎? 這里說說區別

  • 直接用_xxx訪問實例變量而不用點語法可以繞過OC的"方法派發", 效率比用點語法來訪問快
  • 直接用_xxx訪問實例變量而不用點語法不會調用setter方法, 所以不會觸發KVO(Key Value Observing), 同時如果你訪問的該屬性是聲明為copy的屬性, 則不會進行拷貝, 而是直接保留新值, 釋放舊值.
  • 使用點語法訪問有助于debug, 因為可以在setter或getter中增加斷點來監控方法的調用
  • 屬性使用懶加載時, 必須使用點語法, 否則實例變量永遠不會初始化(因為懶加載實際就是調用getter方法, 直接訪問實例變量繞過了該方法, 所以該變量則永遠為nil)

綜上, 比較折中的方法就是

  • 寫入實例變量時, 用setter
  • 讀取實例變量時, 直接訪問
對象等同性

比較兩個對象是否相同.
我們可以重寫isEqual方法自定義對象等同的條件

類族模式

Objective-C的系統框架中普遍使用此模式, 用子類來隱藏"抽象基類"的內部實現細節.
我們肯定使用過UIButton的這個類方法
+ (UIButton *)buttonWithType:(UIButtonType)type;

這就是UIButton類實現的"工廠方法", 根據傳入的枚舉創建并返回合乎條件的子類.

Foundation框架中大部分容器類都是類族, 如NSArray與NSMutableArray, NSSet與NSMutableSet, NSDictionary與NSMutableDictionary.

用isKindOfClass方法可以判斷對象所屬的類是否位于類族之中.

在類族中實現子類時所需遵循的規范一般都會定義于基類的文檔之中, 使用前應先看看.

具體類族的使用方法大家請看書~~

在既有類中使用關聯對象存放自定義數據

在類的內部利用哈希表映射技術, 關聯一個與該類毫無耦合的對象.
使用場景

  • 為現有的類添加私有變量以幫助實現細節
  • 為現有的類添加公有屬性
  • 為KVO創建一個關聯的觀察者

鑒于書中所說, 容易出現循環引用, 以及關聯對象釋放和移除不同步等缺陷,
使用關聯對象這一解決方案總是不到萬不得已都不用的, 所以這里只提供兩篇文章, 感興趣的話大家可以去了解了解.
Associated Objects
Objective-C Associated Objects 的實現原理

消息發送和轉發機制

OC的消息發送和轉發機制是深入了解OC這門語言的必經之路. 下面我們就來學習學習這個消息發送和轉發機制的神奇之處.

objc_msgSend

在解釋OC消息發送之前, 最好先理解C語言的函數調用方式. C語言使用"靜態綁定", 也就是說在編譯器就能決定運行時所應調用的函數. 如下代碼所示

void run() {
    // run
}

void study() {
    // study
}

void doSomething(int type) {
    if (type == 0) {
        run();
    } else {
        study();
    }
}

如果不考慮內聯, 那么編譯器在編譯代碼的時候就已經知道程序中有run和study這兩個函數了, 于是會直接生成調用這些函數的指令. 如果將上述代碼改寫成這樣呢?

void run() {
    // run
}

void study() {
    // study
}

void doSomething(int type) {
    void (*func)();
    if (type == 0) {
        func = run;
    } else {
        func = study;
    }
    func();
}

這就是"動態綁定".

在OC中, 如果向某對象發送消息, 那就會使用動態綁定機制來決定需要調用的方法. OC的方法在底層都是普通的C語言函數, 所以對象收到消息后究竟要調用什么函數完全由運行時決定, 甚至可以在運行時改變執行的方法.

現在開始來探索OC的消息機制

// person : receiver(消息接收者)
// read : selector(選擇子)
// 選擇子 + 參數 = 消息
[person read:book];

編譯器會將以上代碼編譯成以下代碼

// objc_msgSend方法原型為 void objc_msgSend(id self, SEL cmd, ...)
// self : 接收者
// cmd : 選擇子
// ... : 參數, 參數的個數可變
objc_msgSend(person, @selector(read:), book);

objc_msgSend會根據接收者和選擇子的類型來調用適當的方法, 流程如下

  1. 查找接收者的所屬類的cache列表, 如果沒有則下一步
  2. 查找接收者所屬類的"方法列表"
  3. 如果能找到與選擇子名稱相符的方法, 就跳至其實現代碼
  4. 找不到, 就沿著繼承體系繼續向上查找
  5. 如果能找到與選擇子名稱相符的方法, 就跳至其實現代碼
  6. 找不到, 執行"消息轉發".

那么找到與選擇子名稱相符的方法, 就跳至其實現代碼這一步是怎么實現的呢? 這里又要引出一個函數原型了

<return_type> Class_selector(id self, SEL _cmd, ...);

真實的函數名可能有些出入, 不過這里志在用該原型解釋其過程, 所以也就無所謂了.
每個類里都有一張表格, 其中的指針都會指向這種函數, 而選擇子的名稱則是查表時所用的key. objc_msgSend函數正是通過這張表格來尋找應該執行的方法并跳至其實現的.

方法底層實現

乍一看覺得調用一個方法原來要這么多步驟, 豈不是很費時間? 不著急~ objc_msgSend會將匹配結果緩存在"快速映射表"里, 每個類都有這樣一塊緩存, 下次調用相同方法時, 就能很快查找到實現代碼了.

消息發送的其他方法

  • objc_msgSend_stret : 消息要返回結構體, 則由此函數處理.
  • objc_msgSend_fpret : 消息要返回浮點數, 則由此函數處理.
  • objc_msgSendSuper : 給超類發消息.

消息轉發

上面我們曾說過, 如果到最后都找不到, 則進入消息轉發

  • 動態方法解析 : 先問接收者所屬的類, 你看能不能動態添加個方法來處理這個"未知的選擇子"? 如果能, 則消息轉發結束.
  • 備胎(后備接收者) : 請接收者看看有沒有其他對象能處理這條消息? 如果有, 則把消息轉給那個對象, 消息轉發結束.
  • 完整的消息轉發 : 備胎都搞不定了, 那就只能把該消息相關的所有細節都封裝到一個NSInvocation對象, 再問接收者一次, 快想辦法把這個搞定了. 到了這個地步如果還無法處理, 消息轉發機制也無能為力了.
動態方法解析 :

對象在收到無法解讀的消息后, 首先調用其所屬類的這個類方法 :

+ (BOOL)resolveInstanceMethod:(SEL)selector 
// selector : 那個未知的選擇子
// 返回YES則結束消息轉發
// 返回NO則進入備胎

假如尚未實現的方法不是實例方法而是類方法, 則會調用另一個方法resolveClassMethod:

備胎 :

動態方法解析失敗, 則調用這個方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那個未知的選擇子
// 返回一個能響應該未知選擇子的備胎對象

通過備胎這個方法, 可以用"組合"來模擬出"多重繼承".

完整的消息轉發 :

備胎也無能為力了, 只能把消息包裝成一個對象, 給接收者最后一次機會, 搞不定就不搞了!

- (void)forwardInvocation:(NSInvovation *)invocation
// invocation : 封裝了與那條尚未處理的消息相關的所有細節的對象

在這里能做的比較現實的事就是 : 在觸發消息前, 先以某種方式改變消息內容, 比如追加另外一個參數, 或是改變選擇子等等. 實現此方法時, 如果發現某調用操作不應該由本類處理, 可以調用超類的同名方法. 則繼承體系中的每個類都有機會處理該請求, 直到NSObject. 如果NSObject搞不定, 則還會調用doesNotRecognizeSelector:來拋出異常, 此時你就會在控制臺看到那熟悉的unrecognized selector sent to instance..

消息轉發

盡量在第一步就把消息處理了, 因為越到后面所花代價越大.

Method Swizzling

被稱為黑魔法的一個方法, 可以把兩個方法的實現互換.
如上文所述, 類的方法列表會把選擇子的名稱映射到相關的方法實現上, 使得"動態消息派發系統"能夠據此找到應該調用的方法. 這些方法均以函數指針的形式來表示, 這種指針叫做IMP,
id (*IMP)(id, SEL, ...)

NSString類的選擇子映射表

OC運行時系統提供了幾個方法能夠用來操作這張表, 動態增加, 刪除, 改變選擇子對應的方法實現, 甚至交換兩個選擇子所映射到的指針. 如,

經過一些操作后的NSString選擇子映射表

如何交換兩個已經寫好的方法實現?

// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交換實現
void method_exchangeImplementations(Method m1, Method m2)

通過Method Swizzling可以為一些完全不知道其具體實現的黑盒方法增加日志記錄功能, 利于我們調試程序. 并且我們可以將某些系統類的具體實現換成我們自己寫的方法, 以達到某些目的. (例如, 修改主題, 修改字體等等)

類對象

OC中的類也是對象的一種, 你同意嗎?

// 對象的結構體
struct objc_object {
    Class isa;
};
// 類的結構體
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
}

根據以上源碼我們可以知道, 其實類本身也是一個對象, 稱之為類對象, 并且類對象是單例, 即在程序運行時, 每個類的Class僅有一個實例.

實例對象的isa指針指向所屬類, 那么類對象的isa指向什么呢? 是元類(metaclass)

類與對象的繼承層級關系圖
isa指針
  • 根據上文消息派發機制我們可以得知, 實例方法是存在類的方法列表里的, 那么實例對象是怎么找到這些方法呢? 沒錯, 答案就是isa指針.
  • 還有我們剛剛得知, 類也是個對象, 那么我們猜測, 類方法是不是存在元類的方法列表里呢? 是的, 因為類相當于"元類"的實例, 所以實例方法當然是存在類的方法列表中. 而類對象中的isa指針當然是用來查找其所屬類(元類)的了.
用類型信息查詢方法來檢視類繼承體系
// 判斷對象是否為某個 特定類 的實例
- (BOOL)isMemberOfClass:(Class)aClass
// 判斷對象是否為**某類或其派生類**的實例
- (BOOL)isKindOfClass:(Class)aClass

例如, GoodPerson是Person的子類

Person *p = [[Person alloc] init];
GoodPerson *gp = [[GoodPerson alloc] init];
// 判斷p的類型    
[p isMemberOfClass:[Person class]]; // YES
[p isMemberOfClass:[GoodPerson class]]; // NO
[p isKindOfClass:[Person class]]; // YES
[p isKindOfClass:[GoodPerson class]]; // NO
// 判斷gp的類型
[gp isMemberOfClass:[Person class]]; // NO
[gp isMemberOfClass:[GoodPerson class]]; // YES
[gp isKindOfClass:[Person class]]; // YES
[gp isKindOfClass:[GoodPerson class]]; // YES

第3章 : 接口與API設計

這一章講的是一些命名規范, 設計API時的一些注意點.

用前綴避免命名空間沖突

OC沒有其他語言那種內置的命名空間, 所以只能通過前綴來營造一個假的"命名空間". 這里推薦

  • 給類中定義的C語言函數的名字加上類前綴
  • 在類中定義的全局變量要加上類前綴
  • 為自己所開發的程序庫中用到的第三方庫加上前綴

提供"全能初始化方法"

何為全能初始化方法?

  • 所有初始化方法都要調用的方法稱為全能初始化方法.

為什么要提供全能初始化方法?

  • 該方法用來存儲一些內部數據, 初始化一些操作, 這樣當數據存儲機制改變, 只需修改一處的代碼, 無須改動其他初始化方法.

子類的全能初始化應該調用超類的全能初始化方法. 若超類的初始化方法不適用于子類, 那么應該重寫這個超類方法, 并在該方法拋出異常.

實現description方法

我們知道, 利用%@來打印一個對象得到的永遠是<類名 : 內存地址>(NSString, NSDictionary, NSArray等對象除外). 如果我們需要輸出一些我們想要的內容, 那么重寫該方法即可. 應該注意的是不要在description方法輸出self, 會引發死循環.

除了description方法, 還有一個dubug專用的debugDescription方法. 該方法只有在開發者在調試器中用LLDC的"po"指令打印對象時才會調用.

盡量使用不可變對象

我們知道, 如果我們暴露一個可變屬性出去, 然而別人就可以繞過你的API, 隨意地修改該屬性, 進行添加, 刪除操作. 為了加強程序的魯棒性, 我們應該對外公布一個不可變屬性, 然后提供相應的方法給調用者操作該屬性, 而內部修改對象時我們可以使用可變對象進行修改.

// Person.h
@interface Person : NSObject
@property (nonatomic, strong, readonly) NSSet *friends;
- (void)addFriend:(Person *)person;
- (void)removeFriend:(Person *)person;
@end

// Person.m
@interface Person()
{
    NSMutableSet *_internalFriends;
}

@end

@implementation Person
// 返回所有朋友
- (NSSet *)friends
{
    return [_internalFriends copy];
}
// 添加朋友
- (void)addFriend:(Person *)person
{
    [_internalFriends addObject:person];
}
// 移除朋友
- (void)removeFriend:(Person *)person
{
    [_internalFriends removeObject:person];
}
@end

這樣別人拿到的永遠是不可變的NSSet, 而且只能用你給的接口來操作這個set, 你內部依然是使用一個可變的NSMutableSet來做事情, 一舉兩得!

為了使我們的程序變得更加健壯, 我們應該盡量多聲明readonly屬性!

使用清晰而協調的命名方式

我們知道, OC的方法名總是很長, 長得跟句子一樣, 好處很明顯, 那就是一讀就知道該方法是干嘛用的, 劣處嘛, 那就是麻煩了. 這里給幾個方法命名規范

  • 盡量少使用簡稱, 而使用全稱
  • 返回BOOL類型的方法應該用has或is當前綴
  • 在當前對象上操作則方法名應該包含動詞, 如果需要參數著動詞后面跟上
  • 返回對象的方法名, 第一個單詞應該是該返回值的類型

為私有方法加前綴

因為私有方法只在類內部調用, 不像外部方法, 修改會影響到面向外界的那些API, 對于私有方法來說可以隨意修改. 所以為私有方法加前綴可以提醒自己哪些方法可以隨意修改, 哪些不應輕易改動.

  • 給私有方法加上一個p_前綴, 如- (void)p_method;

正確處理錯誤信息

不是有錯就要拋出異常!!!只有在發生了可能致使應用程序崩潰的嚴重錯誤, 才使用異常. OC多使用代理和NSError對象來處理錯誤信息.
NSError對象封裝了三條信息 :

  • Error domain(錯誤范圍, 其類型為字符串, 產生錯誤的根源)
  • Error code(錯誤碼, 其類型為整數, 多為枚舉)
  • User info(用戶信息, 其類型為字典, 有關錯誤的額外信息)

理解NSCopying協議

巧了, 之前我寫過一篇關于copy的文章, 這里就直接引用, 不在贅述了.
小結iOS中的copy


第4章 : 協議和分類

這一章講的協議和分類都是兩個需要重點掌握的語言特性.

通過委托和數據源協議進行對象間通信

委托(delegate), 我還是比較習慣叫代理, 下文就直接說代理了..

代理和數據源, 我們在哪里看到過? 沒錯, UITableView, UICollectionView.
無論是什么對象, 只要遵循了代理協議和數據源協議就都能當一個對象的代理和數據源. 蘋果這么做完全是為了解耦和復用.

而使用代理的時候, 我們是不是總是寫以下這些代碼

if ( [self.delegate respondsToSelector:@selector(someClassDidSomething:)] ) {
    [self.delegate someClassDidSomething:self];
}

那大家有沒有想過, 如果這個方法調用得很頻繁很頻繁, 那么每次調用之前都要問問代理能不能響應這個方法, 不是很影響效率嗎?

我們可以這樣來優化程序效率 -> 把代理能否響應某個方法這一信息緩存起來

這里我們需要用到一個C語言的"位端數據類型". 我們可以把結構體中某個字段所占用的二進制個數設為特定的值

struct data {
    unsigned int fieldA : 8;
    unsigned int fieldB : 4;
    unsigned int fieldC : 2;
    unsigned int fieldD : 1;
}

以上代碼表示fieldA只占用8個二進制位, dieldB占用4個, 如此類推. 那么我們可以根據此特性設計一個代理對象是否響應某代理方法的結構體

@interface Person() {
    struct {
        unsigned int didEat : 1;
        unsigned int didSleep : 1;
    } _delegateFlags;
}
@end

這時我們可以攔截setDelegate方法, 在該方法里面一次過把代理是否響應代理方法全部問個遍, 然后對號入座把各自的BOOL值賦值給_delegateFalgs結構體的對應變量中. 那么我們下次調用代理的相關方法之前就變得優雅多了, 如下:

if ( _delegateFlags.didEat ) {
    [self.delegate didEat:self];
}  

將類的實現代碼分散到便于管理的數個分類之中

如果某個類方法太多, 整個類太臃腫了, 可以根據方法的功能用分類的思想跟方法集分個類, 劃分成易于管理的小塊.

總是為第三方類的分類名稱加前綴

這種情況說起來比較抽象, 直接上代碼, 例如你想要給NSString添加分類,

@interface NSString (HTTP)
- (NSString *)urlEncodedString;
- (NSString *)urlDecodedString;
@end

我們不應該像以上代碼那么做, 因為蘋果說不定哪一天會給NSString加上一個HTTP分類呢? 那么你就相當于復寫了系統的分類了, 這是不允許的. 對應的方法也是, 我們應該為自己為第三方類的分類和方法名加上自己的專用前綴, 如下 :

@interface NSString (JR_HTTP)
- (NSString *)jr_urlEncodedString;
- (NSString *)jr_urlDecodedString;
@end

不要在分類中聲明屬性

除了"class-continuation分類"之外, 其他分類都無法向類中新增實例變量, 它們無法將實現屬性所需的實例變量合成出來. 所以, 請不要在分類中聲明屬性.

分類的目的在于擴展類的功能, 而不是封裝數據.

使用"class-continuation分類"隱藏實現細節

"class-continuation分類"與其他分類不同

  • 它是定義在類的.m文件中的分類
  • 他沒有名稱

"class-continuation分類"的作用 :

  • 定義內部使用的私密類, 不暴露在主接口(.h文件)中
  • 將使用到的C++類放在這里, 可以屏蔽實現細節, 外部甚至不知道你內部寫了C++代碼
  • 把主接口中聲明readonly的屬性擴展為readwrite供類內部使用
  • 把私有方法聲明在這里, 不給外人知道
  • 向類新增實例變量
  • 遵循一些私密協議

通過協議提供匿名對象

有時候對象的類型并不那么重要, 我們只需要保證他能滿足我的需求即可, 不管他是什么類, 這時候可以使用協議來隱藏類的類型, 如下 :
@property (nonatomic, weak) id<JRDelegate> delegate;

我們使用代理時總是這樣, 為什么呢? 只要他遵循了這個協議, 我們甚至不用關心代理是什么, 阿貓阿狗都可以成為我的代理.

而字典中也是說明這一概念. 在字典中, 鍵的標準內存管理語義是"設置時拷貝", 而值的語義則是"設置時保留".
- (void)setObject:(id)object forKey:(id<NSCopying>)key;

我們可以使用這一方法來屏蔽代理對象的實現細節, 使用者只需要這種對象實現了代理方法即可, 其他的你不需要管.


第5章 : 內存管理 與 第6章 : Block與GCD

不知不覺也寫了差不多8千字了, 終于可以歇會了... 哇你千萬不要以為下面的內容不重要. 相反, 他們太重要了, 我花了好多時間去研究內存管理和block, GCD. 還好, 這部分內容我之前已經總結過了, 剛好一一對應.


所以第5章和第6章我會用比較少的筆墨來寫, 因為大部分的內容都已經在文章一開頭所分享的3篇文章里涵蓋了, 這里只把一些漏網之魚補上.

在dealloc方法中只釋放引用并解除監聽

在這個方法里只釋放指針, 解除KVO監聽和NSNotificationCenter通知, 不要做其他耗時操作, 尤其是不要執行異步任務!

用"僵尸對象"調試內存管理問題

在Xcode - Scheme - Run - Diagnostics - 勾選 "Enable Zombie Objects"選項來開啟僵尸對象.

開啟之后, 系統在即將回收對象時, 會執行一個附加步驟, 把該對象轉化為僵尸對象, 而不徹底回收. 這樣你對僵尸對象發送消息后, 控制臺會打印錯誤.

僵尸類 : 如果NSZombieEnabled變量已設置, 那么運行時系統會swizzle原來的dealloc方法, 轉而執行另一方法, 將該類轉換成_NSZombie_OriginalClass, 這里的OriginalClass是原類名.

用handler塊降低代碼分散程度

以前我們總是用代理來監聽一個類內部發生的時. 例如一個下載器類, 下載完畢后通知代理, 下載出錯時通知代理, 這個時候我們的代碼是這樣寫的,

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    downloader.delegate = self;
    [downloader startDownload];
}
#pragma mark - JRDownloaderDelegate
- (void)downloader:(JRDownloader *)downloader didFinishWithData:(NSData *)data
{
    self.data = data;
}

這種辦法沒毛病, 也沒錯, 很好, 但是如果該類中的代理多了起來, 這個類就會變得十分臃腫, 我們可以使用block來寫, 代碼會更加緊致, 開發者調用起來也為方便.如下所示 :

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        self.data = data;
    }];
}

把completion handler塊傳遞給start方法, 當方法調用完畢方法內部就會調用該block把data傳進來. 這種辦法是不是更加聰明呢~

然而我們再想一下, 終于給我們發現代理模式的一個缺點了! 假設我們要同時開啟若干個下載器, 那么在代理方法里面是不是就要對各個下載器進行判斷然后執行對應的操作呢? 很麻煩對吧, 一大堆判斷, if, else, if, else. 然而handler的優勢馬上展現出來了.

- (void)downloadHeaderData {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        // do something
        self.headerData = data;
    }];
}
- (void)downloadFooterData {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        // do something
        self.FooterData = data;
    }];
}

一目了然, 我們根本不需要對哪個下載器進行判斷, 再處理響應的數據, 因為在創建下載器的時候已經設定好了.

而且我們還能用handler很easy地處理下載成功和失敗的情況! 例如,

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        // handler success
    } failureHandler: ^(NSError *error){
        // handler failure
    }];
}

除了這種設計模式以外, 還有一個就是把成功和失敗都放在一個handler中來處理, 例如,

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data, NSError *error){
        if (error) {
            // handler failure
        } else {
            // handler success
        }
    }];
}

這里說說各自的優缺點 :

  • 一個block
    1.代碼長, 比較復雜
    2.失敗的時候還能拿到數據, 做點事情
    3.數據長度不符合時, 需要按下載失敗處理
  • 兩個block
    1.代碼清晰

兩種方法都可以, 蘿卜青菜各有所愛. 不過綜上和結合蘋果的API, 我建議用一個block來同時處理成功和失敗.

補充 : 使用handler的好處還有, 可以通過傳遞多一個隊列的參數, 指定該block在哪個隊列上執行.

用塊引用其所屬對象時, 注意避免循環引用

__weak typeof(self) wself = self;
self.completionHandler = ^(NSInteger result) { 
    [wself removeObserver: wself forKeyPath:@"dog"];
};

這里我就不介紹__weak來避免循環引用了, 要說的是蘋果稱為"Strong-Weak Dance"的一個技術.

我們知道, 使用__weak確實可以避免循環引用. 但是還有點小瑕疵, 假如block是在子線程中執行, 而對象本身在主線程中被銷毀了, 那么block內部的弱引用就會置空(nil). 而這在KVO中會導致崩潰.

Strong-Weak Dance就是針對以上問題的. 使用方法很簡單, 只需要加一行代碼

__weak typeof(self) wself = self;
self.completionHandler = ^(NSInteger result) { 
    __strong typeof(wself) sself = wself;
    [sself removeObserver: sself forKeyPath:@"dog"];
};

這樣一來, block中sself所指向的對象在block執行完畢之前都不會被釋放掉, 因為在ARC下, 只要對象被強引用著, 就不會被釋放.

這里推薦一篇文章, 對Strong-Weak Dance分析得很周到
對 Strong-Weak Dance 的思考


第7章 : 系統框架

我們之所以能夠編寫OS X和iOS的程序, 全是因為有系統框架在背后默默地支持著我們. 系統的框架非常強大, 以置于我們想要實現一些功能的時候, 可以不妨先找找系統有沒有已經幫我們實現好的方法, 往往可以事半功倍.

多用塊枚舉, 少用for循環

for循環

用C語言寫到OC, 我們再熟悉不過了

NSArray *array = /* ... */
for (int i = 0; i < array.count; i++) {
    id obj = array[i];
    // do something with 'obj'
}
快速遍歷

OC 2.0的新特性, 語法簡潔, 好用, 唯一的缺點就是沒有索引

NSArray *array = /* ... */
**for** (id obj **in** array) {
    // do something with 'obj'
}
用OC 1.0 的NSEnumerator來遍歷

這種方法已經過時了, 這里不介紹.

基于塊的遍歷方式

NSArray, NSDictionary, NSSet都有基于block的遍歷方式, 例如數組的 :

NSArray *array = /* ... */
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // do something with 'obj'
    if (shouldStop) {
        *stop = YES;
    }         
}];

不僅能簡單遍歷, 還能控制什么時候退出遍歷. 還有更高級的塊遍歷方法能夠指定選項, 例如反向遍歷, 并行快速遍歷等等.

// 數組的方法 
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
// 字典的方法
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block;

構建緩存時選用NSCache而非NSDictionary

NSCache是Foundation框架中為緩存而生的類. NSCache對比NSDictionary的優點

  • 當系統的資源將要耗盡時, 它可以自動刪減緩存.
  • 先行刪減"最久未使用的對象"
  • NSCache不會"拷貝"鍵, 而是"保留"鍵. 所有在鍵不支持拷貝的情況下比字典方便.
  • 線程安全, 開發者無需自己編寫加鎖代碼
  • 開發者可以操縱緩存刪減其內容的時機, 也就是清理緩存的策略
    1.緩存中的對象總數(countLimit屬性)
    2.緩存中所有對象的總開銷(totalCostLimit屬性)

NSCache還經常會跟NSPurgeableData(NSMutableData的子類)搭配使用, 當NSPurgeableData對象所占內存被系統所丟棄時, 該對象自身也會從緩存中移除.

該類有2個特別的方法

  • beginContentAccess : 告訴它, 現在還不應該丟棄自己所占據的內存
  • endContentAccess : 告訴它, 必要時可以丟棄自己所占據的內存了

緩存使用得當, 將會大大提高應用程序的響應速度. 但并不是什么東西都需要緩存, 只有那種"重新計算起來費勁"的數據, 才值得放入緩存中. 例如從網絡獲取或從磁盤讀取的數據.

精簡 initialize 與 load 的實現代碼

+ (void)load;

該方法特點如下 :

  • 程序一啟動, 就會調用每個類及分類的load方法.
  • 執行子類的load方法之前必定會先執行超類的load方法.
  • 執行分類的load方法之前必定會先執行本類的load方法
  • 該方法不遵從繼承規則, 也就是說如果子類沒有實現load方法, 那么不管其超類有沒有實現該方法, 系統都不會自動調用.
  • 該方法會阻塞主線程!!!!!!!!!!!!!

`+ (void)initialize;

該方法特點如下 :

  • 該方法會在程序首次使用該類之前調用, 且只調用一次. 也就是說該方法是懶加載的, 如果某個類一直沒使用, 就永遠不會調用.
  • 該方法一定會在"線程安全的環境"下執行. 意思就是只有執行該方法的那個線程可以操作類和類實例. 其他線程都要先阻塞, 等著該方法執行完.
  • 該方法遵從繼承規則, 子類如果沒實現它, 則會調用超類的實現代碼.

回到主題, 為什么initialize 與 load的代碼要盡量精簡呢?

  • load方法會阻塞主線程;
  • initialize方法會阻塞當前線程, 如果該線程恰好是主線程, 你懂的...
  • 開發者無法控制類的初始化時機, 也許將來蘋果會修改類的初始化方式呢?
  • 如果某類的該兩方法引入了其他類, 而那些類又沒初始化, 系統就會迫使其初始化, 而那些類初始化一不小心又剛剛好引入了某類, 則會出現"依賴環"

綜上, 盡量不要重寫load方法, 而initialize方法只應該用來

  1. 設置內部數據, 不應該調用其他方法, 哪怕是本類自己的方法.
  2. 如果單例類在首次使用之前需要做一些操作, ok, 在這里執行吧.

NSTimer會保留其目標對象

這種情況跟block引用self那種情況差不多. 目標對象保留計時器, 計時器反過來又保留對象, 則會導致循環引用.

我們可以利用block或者"Strong-Weak Dance"來解決此問題.


啰哩啰嗦地講了好多好多, 也算是分享了自己的一點看法, 字有點多, 如果有人能耐心看完的話絕壁是真愛哈哈..其實寫文章并不是要寫給誰看, 是對自己語言總結能力, 書寫能力的一種鍛煉, 有句話說得好, 就算沒有人看你的博客, 你也要堅持寫下去. 共勉!


歡迎大家關注@Jerry4me, 我會不定時更新一些學習心得與文章.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容