1. 程式人生 > >Effective Objective-C 2.0 敲門磚

Effective Objective-C 2.0 敲門磚

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

前言

這本書和Objective-C高階程式設計-iOS和OS X多執行緒和記憶體管理實在是iOS開發人員必讀書. 實在是太經典了. 相信懂的人自然懂~

這篇文章只是一個敲門磚, 我會把我覺得應該注意, 值得注意, 該掌握的東西都說一說, 大家不要指望看了這篇文章就不用去看書了, 那是遠遠不夠的, 只是希望各位能借助我這篇文章, 先留個整體的印象, 然後再帶著問題去研讀這本書. 那才能達到最好的效果.

目錄

第1章 : 熟悉Objective-C
第2章 : 物件, 訊息, 執行時
第3章 : 介面與API設計
第4章 : 協議和分類
第5章 : 記憶體管理
第6章 : 塊與大中樞派發(也就是Block與GCD)
第7章 : 系統框架

第1章 : 熟悉Objective-C

1. Objective-C是一門動態語言, 該語言使用的是”訊息結構”而非”函式呼叫”.
  • 訊息結構 : 執行時所執行的程式碼由執行時環境決定
  • 函式呼叫 : 執行時所執行的程式碼由編譯器決定.

也就是說

1 [person run];

給person物件傳送一條run訊息 : 不到程式執行的時候你都不知道他究竟會執行什麼程式碼. 而且, person這個物件究竟是Person類的物件, 還是其他類的物件, 也要到執行時才能確定, 這個過程叫動態繫結

.

2. 堆空間

物件所佔記憶體總是分配在堆空間中. 不能再棧中分配Objective-C物件.

  • 棧空間 : 棧空間的記憶體不用程式設計師管理.
  • 堆空間 : 堆空間的記憶體需要程式設計師管理.
12 NSString *anString=@"Jerry";NSString *anotherString=anString;

以上程式碼的意思是, 在堆空間中建立一個NSString例項物件, 然而棧空間中分配兩個指標分別指向該例項. 如圖,

堆和棧
在類的標頭檔案中儘量少引入其他檔案

在類的標頭檔案中用到某個類, 如果沒有涉及到其類的細節, 儘量用@class向前宣告該類(等於告訴編譯器這是一個類, 其他你先別管)而不匯入該類的標頭檔案以避免迴圈引用和減少編譯時間.

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

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

1234567 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
    1234567 typedefNS_ENUM(NSInteger,UIViewAnimationTransition){UIViewAnimationTransitionNone,UIViewAnimationTransitionFlipFromLeft,UIViewAnimationTransitionFlipFromRight,UIViewAnimationTransitionCurlUp,UIViewAnimationTransitionCurlDown,};
  2. 如果列舉型別的多個選項可能組合使用, 則用NS_OPTIONS
    123456789 typedefNS_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是一門動態語言, 其動態性就由這一章來說明.

理解”屬性”這一概念
1234567 @interfacePerson:NSObject{@publicNSString *_firstName;NSString *_lastName;<ahref='http://www.jobbole.com/members/kaishu6296'>@private</a>NSString *_address;}

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

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

12345678 @interfacePerson:NSObject{@publicNSDate *_birthday;NSString *_firstName;NSString *_lastName;<ahref='http://www.jobbole.com/members/kaishu6296'>@private</a>NSString *_address;}

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

在類中新增另一個例項變數前後的資料佈局圖

有人可能會有疑問, 新增例項變數不是要寫程式碼然後編譯執行程式嗎? 重新編譯後物件佈局不就又變正確了嗎? 錯誤! 正是因為Objective-C是動態語言, 他可以在執行時動態新增例項變數, 那時物件佈局早就已固定不能再更改了.

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

@property, @synthesize, @dynamic

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

  • @property
    1234 @interfacePerson:NSObject@propertyNSString *firstName;@propertyNSString *lastName;@end

以上程式碼編譯器會幫我們分解成setter和getter方法宣告, 以上程式碼與以下程式碼等效

123456 @interfacePerson:NSObject-(NSString *)firstName;-(void)setFirstName:(NSString *)firstName;-(NSString *)lastName;-(void)setLastName:(NSString *)lastName;@end
  • @synthesize
123 @implementation Person@synthesize firstName;@end

以上程式碼相當於給Person類新增一個_firstName的例項變數併為該例項變數生成setter和getter方法的實現(存取方法).

可以利用@synthesize給例項變數取名字(預設為_xxx, 例如@property宣告的是name, 則生成的是_name的例項變數)

123 @implementation Person@synthesize firstName=myFirstName;@end

以上程式碼就是生成myFirstName的例項變量了. 由於OC的命名規範, 不推薦這麼做. 沒必要給例項變數取另一個名字.

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

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

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

上程式碼

12345678910111213141516171819 // Person.h@interfacePerson:NSObject@property(nonatomic,copy)NSString *name;@end-------------------------------------------// Person.m@implementation Person@dynamic name;@end-------------------------------------------// main.mintmain(intargc,constchar*argv[]){Person *p=[[Person alloc]init];p.name=@"Jerry";return0;}-------------------------------------------// 程式崩潰, 控制檯輸出-[Person setName:]:unrecognized selector sent toinstance

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

總結下

在現在的編譯器下,

  1. @property會為屬性生成setter和getter的方法宣告, 同時呼叫@synthesize ivar = _ivar生成_ivar例項變數
  2. 手動呼叫@synthesize可以用來修改例項變數的名稱
  3. 手動呼叫@dynamic可以告訴編譯器: 不要自動建立實現屬性所用的例項變數, 也不要為其建立存取方法的實現.
  4. 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的這個類方法

1 +(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語言使用”靜態繫結”, 也就是說在編譯器就能決定執行時所應呼叫的函式. 如下程式碼所示

12345678910111213 voidrun(){// run}voidstudy(){// study}voiddoSomething(inttype){if(type==0){run();}else{study();}}

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

123456789