# Effective Objective-C 2.0 Tips 總結 Chapter 1 & Chapter 2
下面只是對讀到的所有 Tips 結合我平時開發中遇到的問題進行總結,每一個 Tips 和書中的每一條對應,本文的目的是去掉書中的大部分討論的內容,讓人能夠馬上使用這些 Tips,建議閱讀過原書后食用更佳。
## Chapter 1 熟悉 Objective-C
- Tips 1 Objective-C 的起源
- Objective-C 是從 C 語言演化而來,有 C 的一些基礎會有很大幫助
- Tips 2 頭文件中減少引用
- 減少在類的頭文件中 import 其他頭文件,如果使用其他類,那么使用`@class ClassName;`來進行**Forward Declaring**
- 對于協議,每個協議放到對應的頭文件,使用時候引用
- 對于委托協議(比如 `UITableView` 和 `UITableViewDelegate`)因為只有與委托類放在一起才有意義,所以就不用單獨分離頭文件,應該放到定義 `UITableView` 的頭文件中
- Tips 3 使用字面量
- 對于 `NSString`,`NSNumber`,`NSDictionary` 和 `NSArray` 使用類似 `@"String"`,`@1`,`@[]` 和 `@{}` 不要使用等價方法
- Tips 4 使用類型常量
- 定義常量時,使用類型常量不要使用 `#define`,比如:
```
// 使用如下的方式定義
static const NSInteger kInteger = 1;
// 而不是
#define SOME_INTEGER 1
```
這樣可以給編譯器類型信息,在編譯時和開發時能夠進行類型檢查
- 每一個 m 文件都是一個編譯單元
- 使用`static` 聲明表示在本編譯單元有效,若需要將變量放到全局有效,那么需要使用 `extern`
- 使用 `const` 表示常量不會被修改
- Tips 5 使用枚舉表示狀態,選項,狀態碼
- 使用 `NS\_ENUM` 宏定義枚舉,因為枚舉是按順序的,也就是枚舉值是1,2,3…… 這樣的
- 使用 `NS\_OPTION` 定義選項,因為選項是按位的,也就是選項是通過 `1 << 0`,`1 << 1` 這樣來定義的,表示1右移
## Chapter 2 對象,消息,運行時
- Tips 6 理解屬性
- 使用屬性,而不是實例變量,在代碼中使用點(`.`)操作符訪問屬性
- 屬性會生成對應的實例變量,一般是屬性名前加下劃線,也可以在類的實現代碼中通過 `@synthesize` 來指定,例如:`@synthesize firstName = _firstName`
- 使用 `@dynamic` 告訴編譯器不需要生成對應的getter 和 setter
- 屬性的 attribute 會影響編譯器生成的代碼
- atomic / nonatomic,原子性,一般我們都使用 `nonatomic` 因為 iOS 的屬性鎖開銷很大,另外 `atomic` 并不能保證線程安全
- readwrite / readonly,讀寫或是只讀
- 內存管理要注意的
- assign,簡單類型直接賦值
- strong,表示持有
- weak,不持有,在對象被釋放時屬性將會變成 `nil`
- unsafe_unretained,不持有,在對象被釋放時屬性不會變成 `nil`
- copy,設置屬性時會調用對象的 `-copy` 方法獲得新的對象,建議所有不可變的 `NSString`,`NSArray`,`NSDictionary` 都使用這個方法,可變的類型不可以使用這個方法
- getter=name / setter=name,指定 getter 和 setter 方法的名字
- Tips 7 對象內部直接訪問實例變量,設置時通過屬性方法
- 直接訪問實例變量減少方法調用消耗
- 設置通過屬性方法,調用實際的 setter,能夠保證寫入控制和 KVO 的觸發
- 可以在 getter 和 setter 方法中加入斷點方便調試
- 惰性初始化的變量,因為需要需要重寫 getter 方法,所以不能使用直接訪問實例變量來訪問
- Tips 8 對象相等
- `==` 只是比較對象指針是否相等,深層的比較需要使用 `-isEqual:` 方法
- 如果 `-isEqual:` 返回返回真,那他們的 `-hash` 方法要返回同一個值,但是 `-hash` 方法返回同一個值的兩個對象 `-isEqual:` 不一定為真
- `-hash` 方法用作在集合類型中計算索引,如果所有對象的這個方法都返回同一個值,那么在集合中檢索對象性能會很差,這個方法應該使用計算速度快,并且不容易碰撞的方法實現
- 有特定相等判斷方法的對象,優先使用特定相等判斷方法,可以減少調用次數和對對象進行類型檢查(例如 `NSString` 的 `-isEqualToString:` 方法)
- 放入集合對象中的對象,需要保證 `-hash` 方法得到的值不會變,如果放入集合后,修改集合內對象導致 `-hash` 的值發生變化,那么集合對象是不會知道 `-hash` 值有變化,并且將來會出現奇怪的錯誤
- Tips 9 類簇
- 類簇很有用,可以把實現細節隱藏在抽象的基類中
- 對于使用到類簇的對象,需要使用 `-isKindOfClass:` 來判斷,不可使用 `[object class] == [Class class]` 或者 `-isMemberOfClass:` 來判斷
- 若要繼承類簇中的類,那么需要根據文檔實現對應的方法
- Tips 10 Associated Object
- 可以為已有的對象創建新的屬性
- 設置方法
- `void objc\_setAssociatedObject(id object, void *key, id value, objc\_AssociationPolicy policy)`
- `id objc\_getAssociatedObject(id object, void *key)`
- `id objc\_removeAssociatedObject(id object)`
- 關聯類型和屬性的對應(上面 policy 的值)
- OBJC\_ASSOCIATION\_ASSIGN:assign
- OBJC\_ASSOCIATION\_RETAIN_NONATOMIC:nonatomic, retain
- OBJC\_ASSOCIATION\_RETAIN:retain
- OBJC\_ASSOCIATION\_COPY_NONATOMIC:nonatomic, copy
- OBJC\_ASSOCIATION\_COPY:copy
- key 的值,使用一個 opaque pointer,一般來說使用靜態全局變量
- Tips 11 理解 `objc\_msgSend` 的作用
- Objective-C 中所有的方法,都是 C 函數
- 給某個對象的消息全部都是動態發送的,如下
- `id returnValue = [someObject messageName:parameter]`
- 編譯后,將會使用 `objc\_msgSend` 函數處理消息發送,得到 `id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter)`
- `objc\_msgSend` 會去 `someObject` 的方法列表中對應的函數,如果找不到,那么沿著繼承體系繼續找,還是找不到,那么會進行消息轉發操作(在 Tips 12 講解)
- Objective-C 的運行時已經做了很多保證讓這套機制性能很好
- 其中一個優化就是尾調用優化,Objective-C 的每個對象的方法都是 C 方法,并且有和 `objc\_msgSend` 一樣的原型,這樣在 `objc\_msgSend` 從對象的方法列表中找到對應函數時,可以直接跳轉過去,不需要重新在調用堆棧中插入新的棧幀
- Tips 12 理解消息轉發
- 因為 Objective-C 使用運行時來決定具體調用的方法,所以在運行之前是不知道一個對象是否能響應特點方法的
- 消息轉發是在一個對象收到無法解讀的消息時觸發的機制
- 消息轉發的過程
- 第一步,進行動態方法解析——詢問接收者,所屬的類,看是否能動態添加方法,來處理未知的 selector
- 第二步,第一步無法處理這個 selector 的話進行消息轉發——首先,讓接收者看看是否有對象能處理這個消息,如果有,那么丟給他處理;如果沒有,那么運行時會把所有和消息相關的東西放到一個 `NSInvocation` 對象里面,最后再給接收者一次處理的機會
- 動態方法解析
- 過程,下面兩個過程是漸進的,第一個失敗了,那么執行第二個
- 調用 `+ (BOOL)resolveInstanceMethod:(SEL)selector` 詢問類是否能新增一個實例方法處理這個消息
- 調用 `+ (BOOL)resolveClassMethod:(SEL)selector` 詢問類是否能增加一個類方法來處理這個消息
- 用法,相關方法實現已經寫好,只等著運行時去動態插入到類里面就行了
- 實現 `@dynamic` 屬性
- 消息轉發
- 備選的消息接收者
- 調用 `- (id)forwardingTargetForSelector:(SEL)selector` 詢問接收者是否有另一個對象來處理這個消息
- 可以模擬多重繼承,由對象內的其他對象來處理這個消息,但是從調用者看來,是被調用的對象處理的消息
- 這樣轉發的消息,我們是無法進行操作或是修改消息內容的
- 完整的消息轉發
- 創建 `NSInvocation` 對象,把 selector,target 和參數都放進去
- 消息派發系統(message-dispatch system)調用 `- (void)forwardInvocation:(NSInvocation *)invocation` 方法吧消息指派給目標對象
- 這個方法可以簡單的只修改接收者讓另一個對象去接受處理這個方法,但是這樣做法和使用備選的消息接收者做法是等效的,所以一般來說更多是先修改消息內容,再觸發消息
- 這個方法的如果沒有實現,那么就會調用超類的這個方法,類的繼承體系中的所有類都有機會處理直到 `NSObject`
- `NSObject` 的 `- (void)forwardInvocation:(NSInvocation *)invocation` 只是簡單的調用 `- (void)doesNotRecognizeSelector:` 方法拋出異常,所以一般向對象發送他沒有實現的方法都會通過這個方法拋出異常
- Tips 13 Method Swizzling(原書叫方法調配技術,看到這個名詞,第一句話是什么鬼)
- 很多時候,這個技術被大家稱為黑魔法,但是其實他做的只是在運行時交換方法的實現而已
- 所有的方法都是在對象中是一個 `IMP` 指針,指針原型 `id (*IMP)(id, SEL, ...)`
- 每個類通過一張映射表來映射可相應的 selector 和對應 `IMP` 指針的關系
- 可以做 AOP
- 對方法的操作
- 使用 `void method_exchangeImplementations(Method m1, Method m2)` 來交換兩個方法
- 使用 `Method class_getInstanceMethod(Class aClass, SEL aSelector)` 來獲取類的實例方法
- 常規的 Method Swizzling 做法
```
// 在 Category 的定義文件中增加我們將要用來替換的方法
@interface NSString (MethodSwizzling)
- (NSString *)ms_myLowercaseString;
@end
// 在 Category 的實現文件中,進行交換
@implementation NSString (MethodSwizzling)
- (NSString *)ms_myLowercaseString {
// 在調用原方法前做點其他的事情
// 注意這里并不是遞歸調用,而是因為我們交換了 lowercaseString 和 ms_myLowercaseString 的實現,所以這里調用 ms_myLowercaseString 實際上是在調用 lowercaseString 方法
NSString *s = [self ms_myLowercaseString];
// 在調用完原方法后做點其他的事情
return s;
}
+ (void)load {
Method m1 = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method m2 = class_getInstanceMethod([NSString class], @selector(ms_myLowercaseString));
method_exchangeImplementations(m1, m2);
}
@end
```
- Tips 14 理解類對象
- 每個對象結構體的首個成員變量是 `Class` 類的變量
- `Class` 對象是一個 `objc_class` 的結構體,里面保存了類的元數據
- `Class` 類同樣有元類(metaclass)
- 某個類如果有超類(super class)那么他的 `Class` 對象的元類,也繼承于該類超類的元類
- `-isMemberOfClass:` 可以判斷某個對象是否是某個類的實例
- `-isKindOfClass:` 可以判斷某個對象是否是某個類或其子類的實例
- 使用上面說到的兩個方法類判斷類型,不要直接比較類對象,因為某些對象可能實現了消息裝發功能