本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下: 曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把[
本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下:
- 引言
- 簡介
- 與Runtime交互
- Runtime術語
- 消息
- 動態方法解析
- 消息轉發
- 健壯的實例變數(Non Fragile ivars)
- Objective-C Associated Objects
- Method Swizzling
- 總結
引言
曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把[receiver message]
當成簡單的方法調用,而無視了“發送消息”這句話的深刻含義。其實[receiver message]
會被編譯器轉化為:
1
|
objc_msgSend(receiver, selector)
|
如果消息含有參數,則為:
1
|
objc_msgSend(receiver, selector, arg1, arg2, ...)
|
如果消息的接收者能夠找到對應的selector
,那麼就相當於直接執行了接收者這個對象的特定方法;否則,消息要麼被轉發,或是臨時向接收者動態添加這個selector
對應的實現內容,要麼就乾脆玩完崩潰掉。
現在可以看出[receiver message]
真的不是一個簡簡單單的方法調用。因為這隻是在編譯階段確定了要向接收者發送message
這條消息,而receive
將要如何響應這條消息,那就要看運行時發生的情況來決定了。
Objective-C 的 Runtime 鑄就了它動態語言的特性,這些深層次的知識雖然平時寫代碼用的少一些,但是卻是每個 Objc 程式員需要瞭解的。
簡介
因為Objc是一門動態語言,所以它總是想辦法把一些決定工作從編譯連接推遲到運行時。也就是說只有編譯器是不夠的,還需要一個運行時系統 (runtime system) 來執行編譯後的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc運行框架的一塊基石。
Runtime其實有兩個版本:“modern”和 “legacy”。我們現在用的 Objective-C 2.0 採用的是現行(Modern)版的Runtime系統,只能運行在 iOS 和 OS X 10.5 之後的64位程式中。而OS X較老的32位程式仍採用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的實例變數的佈局時,在早期版本中你需要重新編譯它的子類,而現行版就不需要。
Runtime基本是用C和彙編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這裡下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。
與Runtime交互
Objc 從三種不同的層級上與 Runtime 系統進行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject
類定義的方法,通過對 runtime 函數的直接調用。
Objective-C源代碼
大部分情況下你就只管寫你的Objc代碼就行,runtime 系統自動在幕後辛勤勞作著。
還記得引言中舉的例子吧,消息的執行會使用到一些編譯器為實現動態語言特性而創建的數據結構和函數,Objc中的類、方法和協議等在 runtime 中都由一些數據結構來定義,這些內容在後面會講到。(比如objc_msgSend
函數及其參數列表中的id
和SEL
都是啥)
NSObject的方法
Cocoa 中大多數類都繼承於NSObject
類,也就自然繼承了它的方法。最特殊的例外是NSProxy
,它是個抽象超類,它實現了一些消息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕後小弟去乾。
有的NSObject
中的方法起到了抽象介面的作用,比如description
方法需要你重載它併為你定義的類提供描述內容。NSObject
還有些方法能在運行時獲得類的信息,並檢查一些特性,比如class
返回對象的類;isKindOfClass:
和isMemberOfClass:
則檢查對象是否在指定的類繼承體系中;respondsToSelector:
檢查對象能否響應指定的消息;conformsToProtocol:
檢查對象是否實現了指定協議類的方法;methodForSelector:
則返回指定方法實現的地址。
Runtime的函數
Runtime 系統是一個由一系列函數和數據結構組成,具有公共介面的動態共用庫。頭文件存放於/usr/include/objc
目錄下。許多函數允許你用純C代碼來重覆實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject
類的基礎,但是你在寫 Objc 代碼時一般不會直接用到這些函數的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函數的詳細文檔。
Runtime術語
還記得引言中的objc_msgSend:
方法吧,它的真身是這樣的:
1
|
id objc_msgSend ( id self, SEL op, ... );
|
下麵將會逐漸展開介紹一些術語,其實它們都對應著數據結構。
SEL
objc_msgSend
函數第二個參數類型為SEL
,它是selector
在Objc中的表示類型(Swift中是Selector
類)。selector
是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的數據結構是SEL
:
1
|
typedef struct objc_selector *SEL;
|
其實它就是個映射到方法的C字元串,你可以用 Objc 編譯器命令@selector()
或者 Runtime 系統的sel_registerName
函數來獲得一個SEL
類型的方法選擇器。
不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變數類型不同也會導致它們具有相同的方法選擇器,於是 Objc 中方法命名有時會帶上參數類型(NSNumber
一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。
id
objc_msgSend
第一個參數類型為id
,大家對它都不陌生,它是一個指向類實例的指針:
1
|
typedef struct objc_object *id;
|
那objc_object
又是啥呢:
1
|
struct objc_object { Class isa; };
|
objc_object
結構體包含一個isa
指針,根據isa
指針就可以順藤摸瓜找到對象所屬的類。
PS:isa
指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而是應該用class
方法來確定實例對象的類。因為KVO的實現機理就是將被觀察對象的isa
指針指向一個中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文檔
Class
之所以說isa
是指針是因為Class
其實是一個指向objc_class
結構體的指針:
1
|
typedef struct objc_class *Class;
|
而objc_class
就是我們摸到的那個瓜,裡面的東西多著呢:
1
|
struct objc_class {
|
可以看到運行時一個類還關聯了它的超類指針,類名,成員變數,方法,緩存,還有附屬的協議。
PS:OBJC2_UNAVAILABLE
之類的巨集定義是蘋果在 Objc 中對系統運行版本進行約束的黑魔法,為的是相容非Objective-C 2.0的遺留邏輯,但我們仍能從中獲得一些有價值的信息,有興趣的可以查看源代碼。
Objective-C 2.0 的頭文件雖然沒暴露出objc_class
結構體更詳細的設計,我們依然可以從Objective-C 1.0 的定義中小窺端倪:
在objc_class
結構體中:ivars
是objc_ivar_list
指針;methodLists
是指向objc_method_list
指針的指針。也就是說可以動態修改*methodLists
的值來添加成員方法,這也是Category實現的原理,同樣解釋了Category不能添加屬性的原因。關於二級指針,可以參考這篇文章。而最新版的 Runtime 源碼對這一塊的描述已經有很大變化,可以參考下美團技術團隊的深入理解Objective-C:Category。
PS:任性的話可以在Category中添加@dynamic
的屬性,並利用運行期動態提供存取方法或乾脆動態轉發;或者乾脆使用關聯度對象(AssociatedObject)
其中objc_ivar_list
和objc_method_list
分別是成員變數列表和方法列表:
1
|
struct objc_ivar_list {
|
如果你C語言不是特別好,可以直接理解為objc_ivar_list
結構體存儲著objc_ivar
數組列表,而objc_ivar
結構體存儲了類的單個成員變數的信息;同理objc_method_list
結構體存儲著objc_method
數組列表,而objc_method
結構體存儲了類的某個方法的信息。
最後要提到的還有一個objc_cache
,顧名思義它是緩存,它在objc_class
的作用很重要,在後面會講到。
不知道你是否註意到了objc_class
中也有一個isa
對象,這是因為一個 ObjC 類本身同時也是一個對象,為了處理類和對象的關係,runtime 庫創建了一種叫做元類 (Meta Class) 的東西,類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數據。類方法就定義於此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。當你發出一個類似[NSObject alloc]
的消息時,你事實上是把這個消息發給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應消息的類方法。所以當 [NSObject alloc]
這條消息發給類對象的時候,objc_msgSend()
會去它的元類裡面去查找能夠響應消息的方法,如果找到了,然後對這個類對象執行方法調用。
上圖實線是 super_class
指針,虛線是isa
指針。 有趣的是根元類的超類是NSObject
,而isa
指向了自己,而NSObject
的超類為nil
,也就是它沒有超類。
Method
Method
是一種代表類中的某個方法的類型。
1
|
typedef struct objc_method *Method;
|
而objc_method
在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:
1
|
struct objc_method {
|
- 方法名類型為
SEL
,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。 - 方法類型
method_types
是個char
指針,其實存儲著方法的參數類型和返回值類型。 method_imp
指向了方法的實現,本質上是一個函數指針,後面會詳細講到。
Ivar
Ivar
是一種代表類中實例變數的類型。
1
|
typedef struct objc_ivar *Ivar;
|
而objc_ivar
在上面的成員變數列表中也提到過:
1
|
struct objc_ivar {
|
可以根據實例查找其在類中的名字,也就是“反射”:
1
|
-(NSString *)nameWithInstance:(id)instance {
|
class_copyIvarList
函數獲取的不僅有實例變數,還有屬性。但會在原本的屬性名前加上一個下劃線。
IMP
IMP
在objc.h
中的定義是:
1
|
typedef id (*IMP)(id, SEL, ...);
|
它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP
這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在後面會提到。
你會發現IMP
指向的方法與objc_msgSend
函數類型相同,參數都包含id
和SEL
類型。每個方法名都對應一個SEL
類型的方法選擇器,而每個實例對象中的SEL
對應的方法實現肯定是唯一的,通過一組id
和SEL
參數就能確定唯一的方法實現地址;反之亦然。
Cache
在runtime.h
中Cache的定義如下:
1
|
typedef struct objc_cache *Cache
|
還記得之前objc_class
結構體中有一個struct objc_cache *cache
吧,它到底是緩存啥的呢,先看看objc_cache
的實現:
1
|
struct objc_cache {
|
Cache
為方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa
指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優先在Cache
中查找。Runtime 系統會把被調用的方法存到Cache
中(理論上講一個方法如果被調用,那麼它有可能今後還會被調用),下次查找的時候效率更高。這根電腦組成原理中學過的 CPU 繞過主存先訪問Cache
的道理挺像,而我猜蘋果為提高Cache
命中率應該也做了努力吧。
Property
@property
標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property
結構體的指針:
1
|
typedef struct objc_property *Property;
|
可以通過class_copyPropertyList
和 protocol_copyPropertyList
方法來獲取類和協議中的屬性:
1
|
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
|
返回類型為指向指針的指針,哈哈,因為屬性列表是個數組,每個元素內容都是一個objc_property_t
指針,而這兩個函數返回的值是指向這個數組的指針。
舉個慄子,先聲明一個類:
1
|
@interface Lender : NSObject {
|
你可以用下麵的代碼獲取屬性列表:
1
|
id LenderClass = objc_getClass("Lender");
|
你可以用property_getName
函數來查找屬性名稱:
1
|
const char *property_getName(objc_property_t property)
|
你可以用class_getProperty
和 protocol_getProperty
通過給出的名稱來在類和協議中獲取屬性的引用:
1
|
objc_property_t class_getProperty(Class cls, const char *name)
|
你可以用property_getAttributes
函數來發掘屬性的名稱和@encode
類型字元串:
1
|
const char *property_getAttributes(objc_property_t property)
|
把上面的代碼放一起,你就能從一個類中獲取它的屬性啦:
1
|
id LenderClass = objc_getClass("Lender");
|
對比下 class_copyIvarList
函數,使用 class_copyPropertyList
函數只能獲取類的屬性,而不包含成員變數。但此時獲取的屬性名是不帶下劃線的。
消息
前面做了這麼多鋪墊,現在終於說到了消息了。Objc 中發送消息是用中括弧([]
)把接收者和消息括起來,而直到運行時才會把消息與方法實現綁定。
objc_msgSend函數
在引言中已經對objc_msgSend
進行了一點介紹,看起來像是objc_msgSend
返回了數據,其實objc_msgSend
從不返回數據而是你的方法被調用後返回了數據。下麵詳細敘述下消息發送步驟:
- 檢測這個
selector
是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會retain
,release
這些函數了。 - 檢測這個 target 是不是
nil
對象。ObjC 的特性是允許對一個nil
對象執行任何一個方法不會 Crash,因為會被忽略掉。 - 如果上面兩個都過了,那就開始查找這個類的
IMP
,先從cache
裡面找,完了找得到就跳到對應的函數去執行。 - 如果
cache
找不到就找一下方法分發表。 - 如果分發表找不到就到超類的分發表去找,一直找,直到找到
NSObject
類為止。 - 如果還找不到就要開始進入動態方法解析了,後面會提到。
PS:這裡說的分發表其實就是Class
中的方法列表,它將方法選擇器和方法實現地址聯繫起來。
其實編譯器會根據情況在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四個方法中選擇一個來調用。如果消息是傳遞給超類,那麼會調用名字帶有”Super”的函數;如果消息返回值是數據結構而不是簡單值時,那麼會調用名字帶有”stret”的函數。排列組合正好四個方法。
值得一提的是在 i386 平臺處理返回類型為浮點數的消息時,需要用到objc_msgSend_fpret
函數來進行處理,這是因為返回類型為浮點數的函數對應的 ABI(Application Binary Interface) 與返回整型的函數的 ABI 不相容。此時objc_msgSend
不再適用,於是objc_msgSend_fpret
被派上用場,它會對浮點數寄存器做特殊處理。不過在 PPC 或 PPC64 平臺是不需要麻煩它的。
PS:有木有發現這些函數的命名規律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”和“return”。
方法中的隱藏參數
我們經常在方法中使用self
關鍵字來引用實例本身,但從沒有想過為什麼self
就能取到調用當前方法的對象吧。其實self
的內容是在方法運行時被偷偷的動態傳入的。
當objc_msgSend
找到方法對應的實現時,它將直接調用該方法實現,並將消息中所有的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:
- 接收消息的對象(也就是
self
指向的內容) - 方法選擇器(
_cmd
指向的內容)
之所以說它們是隱藏的是因為在源代碼方法的定義中並沒有聲明這兩個參數。它們是在代碼被編譯時被插入實現中的。儘管這些參數沒有被明確聲明,在源代碼中我們仍然可以引用它們。在下麵的例子中,self
引用了接收者對象,而_cmd
引用了方法本身的選擇器:
1
|
- strange
|
在這兩個參數中,self
更有用。實際上,它是在方法實現中訪問消息接收者對象的實例變數的途徑。
而當方法中的super
關鍵字接收到消息時,編譯器會創建一個objc_super
結構體:
1
|
struct objc_super { id receiver; Class class; };
|
這個結構體指明瞭消息應該被傳遞給特定超類的定義。但receiver
仍然是self
本身,這點需要註意,因為當我們想通過[super class]
獲取超類時,編譯器只是將指向self
的id
指針和class
的SEL傳遞給了objc_msgSendSuper
函數,因為只有在NSObject
類才能找到class
方法,然後class
方法調用object_getClass()
,接著調用objc_msgSend(objc_super->receiver, @selector(class))
,傳入的第一個參數是指向self
的id
指針,與調用[self class]
相同,所以我們得到的永遠都是self
的類型。
獲取方法地址
在IMP
那節提到過可以避開消息綁定而直接獲取方法的地址並調用方法。這種做法很少用,除非是需要持續大量重覆調用某方法的極端情況,避開消息發送泛濫而直接調用該方法會更高效。
NSObject
類中有個methodForSelector:
實例方法,你可以用它來獲取某個方法選擇器對應的IMP
,舉個慄子:
1
|
void (*setter)(id, SEL, BOOL);
|
當方法被當做函數調用時,上節提到的兩個隱藏參數就需要我們明確給出了。上面的例子調用了1000次函數,你可以試試直接給target
發送1000次setFilled:
消息會花多久。
PS:methodForSelector:
方法是由 Cocoa 的 Runtime 系統提供的,而不是 Objc 自身的特性。
動態方法解析
你可以動態地提供一個方法的實現。例如我們可以用@dynamic
關鍵字在類的實現文件中修飾一個屬性:
1
|
@dynamic propertyName;
|
這表明我們會為這個屬性動態提供存取方法,也就是說編譯器不會再預設為我們生成setPropertyName:
和propertyName
方法,而需要我們動態提供。我們可以通過分別重載resolveInstanceMethod:
和resolveClassMethod:
方法分別添加實例方法實現和類方法實現。因為當 Runtime 系統在Cache
和方法分發表中(包括超類)找不到要執行的方法時,Runtime會調用resolveInstanceMethod:
或resolveClassMethod:
來給程式員一次動態添加方法實現的機會。我們需要用class_addMethod
函數完成向特定類添加特定方法實現的操作:
1
|
void dynamicMethodIMP(id self, SEL _cmd) {
|
上面的例子為resolveThisMethodDynamically
方法添加了實現內容,也就是dynamicMethodIMP
方法中的代碼。其中 “v@:
” 表示返回值和參數,這個符號涉及 Type Encoding
PS:動態方法解析會在消息轉發機制浸入前執行。如果 respondsToSelector:
或 instancesRespondToSelector:
方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP
的機會。如果你想讓該方法選擇器被傳送到轉發機制,那麼就讓resolveInstanceMethod:
返回NO
。
消息轉發
重定向
在消息轉發機制執行前,Runtime 系統會再給我們一次偷梁換柱的機會,即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector
方法替換消息的接受者為其他對象:
1
|
- (id)forwardingTargetForSelector:(SEL)aSelector
|
畢竟消息轉發要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回 如果此方法返回nil或self,則會進入消息轉發機制(self
,因為那樣會死迴圈。forwardInvocation:
);否則將向返回的對象重新發送消息。
轉發
當動態方法解析不作處理返回NO
時,消息轉發機制會被觸發。在這時forwardInvocation:
方法會被執行,我們可以重寫這個方法來定義我們的轉發邏輯:
1
|
- (void)forwardInvocation:(NSInvocation *)anInvocation
|
該消息的唯一參數是個NSInvocation
類型的對象——該對象封裝了原始的消息和消息的參數。我們可以實現forwardInvocation:
方法來對不能處理的消息做一些預設的處理,也可以將消息轉發給其他對象來處理,而不拋出錯誤。
這裡需要註意的是參數anInvocation
是從哪的來的呢?其實在forwardInvocation:
消息發送前,Runtime系統會向對象發送methodSignatureForSelector:
消息,並取到返回的方法簽名用於生成NSInvocation
對象。所以我們在重寫forwardInvocation:
的同時也要重寫methodSignatureForSelector:
方法,否則會拋異常。
當一個對象由於沒有相應的方法實現而無法響應某消息時,運行時系統將通過forwardInvocation:
消息通知該對象。每個對象都從NSObject
類中繼承了forwardInvocation:
方法。然而,NSObject
中的方法實現只是簡單地調用了doesNotRecognizeSelector:
。通過實現我們自己的forwardInvocation:
方法,我們可以在該方法實現中將消息轉發給其它對象。
forwardInvocation:
方法就像一個不能識別的消息的分發中心,將這些消息轉發給不同接收對象。或者它也可以象一個運輸站將所有的消息都發送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應也沒有錯誤。forwardInvocation:
方法也可以對不同的消息提供同樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不同的對象鏈接到消息鏈的能力。
註意: forwardInvocation:
方法只有在消息接收對象中無法正常響應消息時才會被調用。 所以,如果我們希望一個對象將negotiate
消息轉發給其它對象,則這個對象不能有negotiate
方法。否則,forwardInvocation:
將不可能會被調用。
轉發和多繼承
轉發和繼承相似,可以用於為Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。
這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中Warrior
和Diplomat
沒有繼承關係,但是Warrior
將negotiate
消息轉發給了Diplomat
後,就好似Diplomat
是Warrior
的超類一樣。
消息轉發彌補了 Objc 不支持多繼承的性質,也避免了因為多繼承導致單個類變得臃腫複雜。它將問題分解得很細,只針對想要借鑒的方法才轉發,而且轉發機制是透明的。
替代者對象(Surrogate Objects)
轉發不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背後是強大的男人,畢竟女人遇到難題都把它們轉發給男人來做了。這裡有一些適用案例,可以參看官方文檔。
轉發與繼承
儘管轉發很像繼承,但是NSObject
類不會將兩者混淆。像respondsToSelector:
和 isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉發鏈。比如上圖中一個Warrior
對象如果被問到是否能響應negotiate
消息:
1
|
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
|
結果是NO
,儘管它能夠接受negotiate
消息而不報錯,因為它靠轉發消息給Diplomat
類來響應消息。
如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior
繼承到了Diplomat
的negotiate
方法,你得重新實現 respondsToSelector:
和 isKindOfClass:
來加入你的轉發演算法:
1
|
- (BOOL)respondsToSelector:(SEL)aSelector
|
除了respondsToSelector:
和 isKindOfClass:
之外,instancesRespondToSelector:
中也應該寫一份轉發演算法。如果使用了協議,conformsToProtocol:
同樣也要加入到這一行列中。類似地,如果一個對象轉發它接受的任何遠程消息,它得給出一個methodSignatureForSelector:
來返回準確的方法描述,這個方法會最終響應被轉發的消息。比如一個對象能給它的替代者對象轉發消息,它需要像下麵這樣實現methodSignatureForSelector:
:
1
|
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
|
健壯的實例變數(Non Fragile ivars)
在 Runtime 的現行版本中,最大的特點就是健壯的實例變數。當一個類被編譯時,實例變數的佈局也就形成了,它表明訪問類的實例變數的位置。從對象頭部開始,實例變數依次根據自己所占空間而產生位移:
上圖左邊是NSObject
類的實例變數佈局,右邊是我們寫的類的佈局,也就是在超類後面加上我們自己類的實例變數,看起來不錯。但試想如果哪天蘋果更新了NSObject
類,發佈新版本的系統的話,那就悲劇了:
我們自定義的類被划了兩道線,那是因為那塊區域跟超類重疊了。唯有蘋果將超類改為以前的佈局才能拯救我們,但這樣也導致它們不能再拓展它們的框架了,因為成員變數佈局被死死地固定了。在脆弱的實例變數(Fragile ivars) 環境下我們需要重新編譯繼承自 Apple 的類來恢復相容性。那麼在健壯的實例變數下會發生什麼呢?
在健壯的實例變數下編譯器生成的實例變數佈局跟以前一樣,但是當 runtime 系統檢測到與超類有部分重疊時它會調整你新添加的實例變數的位移,那樣你在子類中新添加的成員就被保護起來了。
需要註意的是在健壯的實例變數下,不要使用sizeof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
來代替。
Objective-C Associated Objects
在 OS X 10.6 之後,Runtime系統讓Objc支持向對象動態添加變數。涉及到的函數有以下三個:
1
|
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
|
這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量: