Effective Objective-C 2.0 學習筆記(2)

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

Effective Objective-C 2.0 學習筆記(2)

設置關聯對象

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

取關聯對象值

id objc_getAssociatedObject(id object, void *key)

移除指定對象的全部關聯對象

void objc_removeAssociatedObjects(id object)

要點

定義關聯對象可指定內存管理語義,用以模仿定義屬性時所採用的“擁有關係”與“非擁有關係”

只有在其他做法不可行時才應選用關聯對象,因為這種做法通常會引入難於查找的bug

第十一條 理解objc_msgSend的作用

C語言使用“靜態綁定”,在編譯期就能決定運行時調用的函數

在Objective-C中,如果向某個對象傳遞消息,那就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法都是普通的C語言函數,然而對象收到消息後,究竟該調用哪個方法則完全於運行期決定,甚至可以在程序運行時改變,這些特性使得Objective-C成為一門真正的動態語言。

id returnValue = [someObject messageName:parameter];

someObject: 接受者(receiver)

messageName: : 選擇子(selector)

選擇子與參數 合起來稱為“消息(message)”

編譯器看到此消息後,將其轉換為一條標準的C語言函數調用,其原型

void objc_msgSend(id self, SEL cmd, ...)

轉換後:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

該方法需要在接收者所屬的類中搜尋其“方法列表”,如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若找不到,就沿著繼承體系繼續向上查找,等找到合適的方法之後再跳轉,如果最終還是找不到相符的方法,就執行“消息轉發(message forwarding)”

objc_msgSend會將匹配結果緩存在“快速映射表”(fast map)裡,每個類都有這樣一個緩存,稍後還向該類發送與選擇子相同的消息,會快很多,但還是不如“靜態綁定的函數操作”那樣迅速。

消息派發並非應用程序的瓶頸所在。假如是瓶頸,可以只編寫純C函數

objc_msgSend等函數一旦找到應該調用的方法實現後,就會“跳轉過去。”之所以能這樣,因為Objective-C對象的每個方法都可以視為簡單的C函數,其原型如下

<return> Class_selector(id self, SEL _cmd, ...)/<return>

每個類裡面都有一張表,其中的指針都指向這種函數,而選擇子的名稱是查表時所用的“鍵”。objc_msgSend正式通過這張表格來尋找應該執行的方法並跳至實現的。

要點

發給某對象的全部消息都要由“動態消息派發系統”(dynamic message dispatch system)來處理,該系統會查出對應的方法,並執行其代碼。

第十二條 理解消息轉發機制

當對象接收到無法解讀的消息後,就會啟動“消息轉發(message forwarding)”機制,程序員可經由此過程告訴對象應該如何處理未知消息。

消息轉發分為兩大階段。第一階段先徵詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子(unknown selector)”,這叫做“動態方法解析”(dynamic method resolution)。

第二階段涉及“完整的消息轉發機制”(full forwarding mechanism)。如果運行期系統已經把第一階段執行完了,那麼接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的消息了。此時,運行期系統會請求接收者以其他手段來處理與消息相關的方法調用。這又細分為兩小步

a、首先,接收者看看有沒有其他對象能處理這條消息。若有,則運行期系統會把消息轉給那個對象,消息轉發過程結束。

b、若沒有“備援的接收者”(replacement receiver),則啟動完整的消息轉發機制,運行期系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最後一次機會,令其設法解決當前還未處理的這條消息。

對象在收到無法解讀的消息後,首先將調用其所屬類的下列類方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

+ (BOOL)resolveClassMethod:(SEL)selector

使用這種方法的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類裡面就可以了。此方案常用來實現@dynamic 屬性。

備援接收者:當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。該步驟對應的處理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

完整的消息轉發

- (void)forwardInvocation:(NSInvocation *)invocation

這個方法可以實現得很簡單:只需改變調用目標,使消息在新目標得以調用即可。然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以很少有人採用這麼簡單的實現方式。比較有用的實現方式為:在出發消息前先以某種方式改變消息內容,比如追加另外一個參數,或者改變選擇子,等等。

Effective Objective-C 2.0 學習筆記(2)

接收者在每一步中均有機會處理消息。步驟越往後,處理消息的代價就越大。

第十三條 用“方法調配技術” 調試 “黑盒方法”

類的方法列表會把選擇子的名稱映射到相關的方法實現上,使得“消息派發系統”能夠根據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這種指針叫做IMP,其原型如下:

id (*IMP)(id, SEL, ...)

交換方法實現,可用下列函數:

void method_exchangeImpentations(Method m1, Method m2)

此函數的兩個參數表示待交換的兩個方法實現,而方法實現則可通過下列函數獲得

Method class_getInstanceMethod(Class aClass, SEL aSelector)

要點:

使用另一份實現來替換原有的方法實現,這道工序叫做“方法調配”,開發者常用此技術向原有實現中添加新功能。

一般來說,只有調試程序的時候才需要在運行期修改方法實現,這種做法不宜濫用

第十四條 理解“類對象”的用意

描述Objective-C對象所用的數據結構定義在運行期程序庫的頭文件裡,id類型本身也在定義在這裡:

typedef struct objc_object {

Class isa;

} *id;

typedef struct objc_class *Class;

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;

}

Effective Objective-C 2.0 學習筆記(2)

每個類僅有一個“類對象”,而每個“類對象”僅有一個與之相關的“元類”

isMemberOfClass: 判斷對象是否為某個特定類的實例

isKindOfClass: 對象是否為某類或其派生類的實例

類對象是“單例”,在應用程序範圍內,每個類的Class僅有一個實例。

要點

每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構成類的集成體系

如果對象類型無法在編譯器確定,那麼就應該使用類型信息查詢方法來探知

儘量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能

第十五條 用前綴避免命名空間衝突

Objective-C沒有其他語言語言那種內置的命名空間機制

使用Cocoa創建應用程序時一定要注意,Apple宣稱其保留使用所有“兩字母前綴”的權利,所以你自己選用的前綴應該是三字母的。

要點

選擇與你的公司,應用程序或二者皆有關聯之名稱作為類名的前綴,並在所有代碼中均使用這一前綴,並在所有代碼中均使用這一前綴

若自己所開發的程序庫中用到了第三房庫,則應為其中的名稱加上前綴

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

如果創建類實例的方法不止一種,那麼這個類就會有多個初始化方法。要再其中選定一個作為全能初始化方法。

要點

在類中提供一個全能初始化方法,並於文檔裡指明。其他初始化方法均應調用此方法

若全能初始化方法與超類不同,則需覆寫超類中的對應方法

如果超類的初始化方法不適用於子類,那麼應該覆寫這個超類方法,並在其中拋出異常

第十七條 實現description方法

- (NSString *)description {

return [NSString stringWithFormat:@"", [self class], self, _firstName, _lastName];

}

//output:

// person = <eocperson>

要點

實現description方法返回一個有意義的字符串,用以描述該實例

若想在調試時打印出更詳細的對象描述信息,則應實現debugDescription方法

第十八條 儘量使用不可變對象

應該儘量把對外公佈出來的屬性設為只讀,而且只在確有必要時才將屬性對外公佈。

有時可能想修改封裝在對象內部的數據,但是卻不想令這些數據為外人所改動。這種情況下,通常做法是在對象內部將readonly屬性重新聲明為readwrite

在定義類的公共API時,還要注意一件事情:對象裡表示各種collection的那些屬性究竟應該設成可變的還是不可變的。例如,用某個類表示個人信息,該類裡還存放了一些引用,指向此人的諸位朋友。把這個人的全部朋友放在一個列表裡,並將其做成屬性,可以添加或刪除此人的朋友,這個屬性需要用可變的set來實現。在這種情況下,通常應該提供一個readonly屬性供外界使用,該屬性返回不可變的set,而此set是內部不可變set的一份拷貝

要點

儘量創建不可變的對象

若某屬性僅可於對象內部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite

不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection

Effective Objective-C 2.0 學習筆記(2)

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

不要吝於使用長方法名。把方法名起得稍微長一點,可以保證其能準確傳達出方法所執行的任務

給方法命名時的注意事項:

1、如果方法的返回值是新創建的,那麼方法名的首個詞應是返回值的類型,除非前面還有修飾語,例如localizedString。屬性的存取方法不遵循這種命名方式,因為一般認為這些方法不會創建新對象,即便有時返回內部對象的一份拷貝,我們也認為那相當於原有的對象。這些存取方法應該按照其所對應的屬性來命名

2、應該把表示參數類型的名詞放在參數前面。

3、如果方法要在當前對象上執行操作,那麼就應該包含動詞,若執行操作時還需要參數,則應該在動詞後面加上一個或多個名詞。

4、不要使用str這種簡稱,應該用string這樣的全稱

5、Boolean屬性應加is前綴。如果某方法返回屬性的Boolean值,那麼應該根據其功能,選用has或is當前綴。

6、將get這個前綴留給那些藉由“輸出參數”來保存返回值的方法。

第二十條 為私有方法名加前綴

與公有方法不同,私有方法不出現在接口定義中。有時可能要在“class-continuation分類”裡聲明私有方法,然而最近修訂的編譯器已經不需要在使用方法前必須先聲明瞭。所以說,私有方法一般只在實現的時候聲明。

要點

給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區分開

不要單用一個下劃線做私有方法前綴,因為這種做法是預留給蘋果公司用的



分享到:


相關文章: