本章節作為Objective-C 2.0運行時系統編程指南的小結;也算是一次對系統性書籍的知識吸收,對零散知識的復習
首先第一個問題:runtime到底是什么?
runtime是一套由C、C++、匯編語言編寫的數據結構和函數
這些函數使得訪問運行時系統成為了可能
本文章包括以下內容:
- 運行時系統的版本和平臺
- 和運行時系統的交互
- 消息
- 動態方法解析
- 消息轉發
- 類型編碼
- 屬性聲明
運行時系統的版本和平臺
早期版本和現在的版本
早期版本是Objective-C 1。 在早期版本中,如果你改變類中的實例變量的布局,你必須重新編譯該類的所有子類
現在的版本是Objective-C 2。 在現在的版本中,如果你改變類中的實例變量的布局,你無需重新編譯該類的子類。
現行版本還支持聲明property的synthesis屬性
平臺
iPhone程序和Mac OS X v10.5及以后的系統中的64位程序使用的都是Objective-C 2.0的版本
其他情況(Mac OS X系統中的32位程序)使用的是早期的版本
和運行時系統的交互
Objective-C 程序有三種途徑和運行時系統交互:
通過Objective-C源碼
大部分情況下,運行時系統會在底層自動運行,我們只需要編寫Objective-C源碼
當我們編譯OC類和方法的時候,編譯器為實現語言動態特性將會自動創建一些數據結構和函數。這些數據結構包含了類的信息,運行時系統的主要功能就是根據OC代碼去發送消息。
說白了,我們的OC代碼,被動的被系統利用到運行時。
通過NSObject類的方法
OC中絕大部分類都是NSObject類的子類,所以擁有了NSObject類的所有行為(NSProxy是一個例外,在消息轉發中有提到)
以下方法,都屬于利用了運行時
- Class 返回對象的類
- isKindOfClass 、isMemberOfClass 檢查對象是否在指定的繼承體系中
- respondsToSelector 檢查對象是否能響應某方法
- conformsToProtocol 檢查對象是否實現了指定的協議方法
- methodForSelector 則返回指定方法實現的地址
通過運行時系統的函數
runtime有一套公開的API,這些API聲明在/usr/include/objc中。用來供開發者使用,提供運行時能力。
這里不列舉了,只是一些函數名而已
消息
本節內容描述了方法如何轉化為對objc_msgSend的調用,如何通過名字來指定一個方法,以及如何使用objc_msgSend函數
獲得方法地址
在明白方法調用時,是通過isa指針->類->父類....->NSObject 這一前提下。
還要知道類中有一個cache ,是用來存放方法緩存的。他是一個hashmap,key值為方法簽名(Selector)
value為函數地址(IMP)。當一個方法被調用時,那么他可能還會被調用,當要頻繁得調用某一方法時,使用方法地址就會大大提高效率
利用NSObject類中的methodForSelector: 方法,你可以獲得一個指向方法實現的指針,并且可以使用該指針,直接調用函數。返回的指針和賦值的變量類型必須完全一致。
下面是一個例子,利用指針來調用setFilled:的方法實現
void (*setter)(id, SEL, BOOL);
int I;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0; i < 1000, i++ ){
setter(targetList[i], @selector(setFilled:), YES);
}
函數指針setter的第一個參數是接受消息的對象self, 第二個參數是方法選標(_cmd) 。這兩個參數是所有方法都有的,是一個隱式參數,但是在函數的表達中必須顯示的給出。
使用methodForSelector: 來避免動態綁定可以減少大部分的開銷
注意:methodForSelector: 是runtime提供的功能,不是OC語言本身的功能
objc_msgSend函數
在OC中,消息是直到運行的時候才和方法實現綁定的。編譯器會把一個方法的表達式:
[receiver message]
轉換成對objc_msgSend函數的調用。 該函數有兩個主要的參數:消息接受者(id類型)和消息對應的方法名字(_CMD類型):
objc_msgSend(receiver, selector)
同時接受消息中的任意數目的參數:
objc_msgSend(receiver, selector, arg1, arg2, ...)
該函數做了動態綁定所需要的一切:
- 它首先找到了方法選標對應的方法實現。 因為不同的類對同一方法可能會有不同的實現,所以找到的方法實現依賴于消息接受者的類型
- 然后將消息接收者對象以及方法中指定的參數傳給找到的方法實現
- 最后,將方法實現的返回值作為該函數的返回值返回。
消息機制的關鍵在于編譯器為類和對象生成的數據結構。每個類的結構中至少包含兩個基本元素:
- 指向父類的指針
- 類的方法表。方發表就是一個hashmap,通過key - value的方式關鍵方法選標和方法實現
當新的對象被創建時,其內存同時被分配,實例變量也同時被初始化。對象的第一個實例變量是一個指向該對象的類的指針,叫isa指針,通過這個指針,找到對象的類及其父類。
當對象收到消息時,消息函數首先根據該對象isa指針找到該對象所對應的類的方發表,并從表中尋找改消息對應的方法選標。如果找不到,objc_msgSend將繼續從父類中尋找,知道NSObject類。一旦找到了方法選標,objc_msgSend則以消息接受者對象為參數調用,調用該選標對應的方法實現。這就是在運行時系統中選擇方法實現的方式。在面向對象編程中,一般稱作方法和消息動態綁定的過程.
為了加快消息的處理過程,runtime會將使用過的方法選標和方法實現的地址放入緩存中。每個類都有一個獨立的緩存,同時包括繼承的方法和在該類中定義的方法。消息函數會首先檢查消息接受者對象對應的類的緩存。如果在緩存中已經有了需要的方法選標,則消息僅僅比函數調用慢一點點,這個過程稱作快速查到,其底層是運行的匯編代碼,其原因有兩點:
- c、c++語言無法做到:傳入一個未知的對象,和一個未知的方法名,跳轉到任意指針
- 此行為是程序運行中最為頻繁的,用匯編,更快
動態方法解析
動態方法解析
如果你需要動態的提供一個方法的實現,你可以通過實現 resolveInstanceMethod: 和 resolveClassMethod: 來動態地實現給定選標的對象方法或者類方法
可以通過resolveInstanceMethod: 將一個dynamicMethodIMP函數作為類方法resolveThisMethodDynamically方法的實現
void dynamicMethodIMP(id self, SEL _cmd) {
//mentation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod: (SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self Class], aSEL, (IMP)dynamicMethodIMP, "v@:")
return YES;
}
return [super resolveInstanceMethod: aSEL]
}
通常來說,消息轉發和動態方法解析是互不相干的。在進入消息轉發機制之前,respondsToSelector: 和 instancesRespondToSelector: 會被首先調用。可以在這個兩個方法中為傳進來的選標提供一個IMP。如果你實現了resolveInstanceMethod: 方法,但還是希望繼續走消息轉發流程,只需要返回NO就可以了。
動態加載
OC可以在運行時鏈接和載入新的類。新載入的類在程序啟動時載入的類并沒有區別。
消息轉發
消息轉發
如果一個對象收到一條為實現的消息,runtime會在拋出錯誤前,給該對象發送一條forwardingInvocation: 消息,該消息的唯一參數是一個NSInvocation類型的對象,該對象封裝了原始的消息和消息的參數。
你可以實現forwardingInvocation: 方法來對沒有實現的消息做一些處理,也可以以其他某種方式來避免錯誤被拋出。正如forwardingInvocation: 的名字所示,它通常用來將消息轉發給其他對象。
關于消息轉發的作用,你可以考慮如下場景:假如你需要設計一個能夠響應 negotiate 方法的對象,并且能夠包括其他類型的對象對消息的響應。通過在 negotiate 方法的實現中將 negotiate 消息轉發給其他對象,很容易達到這個目的。
更進一步,假如你希望你的對象和另外一個類的對象對 negotiate 消息的響應完全一致。一種方式就是讓你的類繼承于它。 但有時候,你的類和此類需要在不同的繼承體系中。雖然你的類無法繼承其他類的 negotiate 方法,你還可以這么做:提供一個方法實現,這個方法實現只是簡單的將 negotiate 消息轉發給其他類的對象:
-(id)negotiate {
if ([someOtherObj respondsTo:@selector(negotiate)]) {
return [someOtherObj negotiate];
}
return self;
}
這種方式不太靈活,特別是有很多消息你都希望傳遞給其他對象時,你必須在每一種消息都做同樣的事情。此外,這種方式不能處理未知的消息。消息類型是死的,但實際上,消息的類型可能會隨著運行時而發生變化。
forwardInvocation: 方法給這個問題提供了一個動態的解決方案: 當一個對象由于沒有相應的方法實現時,runtime會通過 forwardInvocation: 消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation: 方法。而NSObject中的方法實現只是簡單的調用了 doesNotRecognizeSelector: 。通過重寫forwardInvocation: 方法,你可以在該方法實現中將消息轉發給其他對象。
要轉發消息給其他對象,forwardInvocation: 方法所必須做的有:
- 決定將消息轉發給誰
- 將消息和原來的參數一起轉發出去
消息可以通過invokeWithTarget: 方法來轉發:
- (void)forwardInvocation(NSInvocation *)anInvocation {
if ([someObj respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget: someObj]
} else {
[super forwardInvocation: anInvocation]
}
}
轉發消息后的返回值將返回給原來的消息發送者
forwardInvocation: 方法就像一個不能識別消息的分發中心,將這些消息轉發給不同接受對象;或者它也可以想消息都發送給同一個接受對象;可以將一個消息翻譯成另外一個消息,或者簡單的 吃掉 某些消息,因此沒有響應也沒有錯誤。
注意:forwardInvocation: 方法只有在消息接受者不能正常響應消息時才會被調用。所以如果你希望你的對象將negotiate消息轉發給其他對象,你的對象不能有negotiate方法的實現。否則將不會調用forwardInvocation
消息轉發和多重繼承
消息轉發很像繼承,一個對象通過轉發來響應消息,看起來就是像是從別的類繼承了方法實現一樣。
在上圖中,warrior類的一個實例對象將 negotiate 消息轉發給 Diplomat 類的一個實例。看起來,warrior類似乎和Diplomat類一樣。響應 negotiate 消息,并且行為和Diplomat一樣(實際上是Diplomat類響應了該消息)
轉發消息的對象看起來有兩個繼承分支:自己的和響應消息的對象的。在上面的例子中,warrior看起來同時繼承自己的父類和Diplomat類。
消息轉發提供了多重繼承的很多特性。然后兩者又有很大不同:多重繼承是將不同的行為封裝到單個的對象中,有可能導致龐大的,復雜的對象。而消息轉發是將問題分解到更小的對象中,但是又以一種對消息發送對象來說完全透明的方式將這些對象聯系起來。
消息轉發和類繼承
消息轉發很像繼承,但它不是繼承。
例如在NSObject類中,方法respondsToSelector: 和 isKindOfClass: 只會出現在繼承鏈中。例如向一個Warrior類的對象發送此消息:
if ([aWarrior respondsToSelector:@selector(negatiate)]) {
}
返回值是NO,盡管該對象能夠接受和響應negotiate
如果你需要其返回YES ,比如:
使用消息轉發來創建一個代理對象以擴展某個類的能力,這里的消息轉發必須和繼承一樣,盡可能的對用戶透明。如果你希望代理對象看起來就像是繼承自代表它代表的對象一樣,你需要重新實現respondsToSelector: 方法和isKindOfClass: 方法
- (BOOL)respondsToSelector:(SEL)aSelector </pre>
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了respondsToSelector: 和 isKindOfClass: 外,instanceRespondToSelector: 方法也必須重新實現。如果你使用的是協議類,還需要重寫conformsToProtocol: 方法。如果對象需要轉發遠程消息,則methodSignatureForSelector: 方法必須能夠返回實際響應消息的方法的描述。
注意:消息轉發是一個比較高級的技術,僅適用于沒有其他更好的解決辦法的情況。它并不是用來代替繼承的。
類型編碼
為了和運行時系統協作,編譯器將方法的返回值類型和參數類型都編碼成一個字符串。并且和方法選標關聯在一起。
上圖為對應表
屬性聲明
當編譯器遇到一個Property聲明時,編譯器會產品一些元數據與屬性所在的類或者協議類關聯。 我們可以通過一些函數訪問它們。每個類或者協議類都維護了一個聲明了的屬性列表。
屬性類型和相關函數
函數class_copyPropertyList 和 protocol_copyPropertyList 來或者類或者協議類中的屬性列表
函數property_getName 獲取屬性的名字
函數property_getAttributes 可以獲取屬性的名字和@encode編碼
還有更多函數,這里只是舉例說明
關于屬性的相關內容,還有
- 屬性類型編碼
- 屬性特征的描述范例
暫時覺得沒有太大的用處,后續用到會更新------
以上就是runtime編程指南學習總結 若有不對,還請指出
碼海無涯,學無止境