Objective-C runtime 介紹
使用 runtime
Objective c 使用系統(tǒng)的 runtime 有三種形式:通過 objective-c 源碼,通過 Foundation framework 里面的 NSObject class 和 直接調(diào)用 runtime 的函數(shù)垢箕。
objective-c 源碼
大多數(shù)場景楞黄,我們僅寫和編譯Objectiv-C 代碼瘟滨,系統(tǒng) runtime 底層被我們使用。編譯包含 Objective-C 的 class 和 methods 的時(shí)候,編譯器創(chuàng)建對應(yīng)的數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)編程語言動(dòng)態(tài)特性的函數(shù)調(diào)用。 創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)中包括 class 和 category 的定義九火,還有 protocol 的聲明,還有對應(yīng)的 methods册招,變量模板和其他從源碼中提取的信息岔激。最重要的 runtime 函數(shù)是我們后面要提到的發(fā)消息(message)。
NSObject methods
很多的 Cocoa 應(yīng)用中的類都是繼承于 NSObject是掰, 所以也繼承了NSObject 中定義的一些方法(NSProxy 不是NSObject 的子類)虑鼎。對于這些類來講,他們的實(shí)例就固有 NSObject 的這些方法和實(shí)現(xiàn)键痛。在一些場景下 NSObject
只是定義了一個(gè)模板炫彩,它自己沒有一些特定的實(shí)現(xiàn)邏輯。
比如 對于 description
方法絮短,NSObject
的實(shí)現(xiàn)邏輯是返回一個(gè)類的描述字符串江兢, 這個(gè)方法主要在使用GDB debug
過程中打印對象內(nèi)容的時(shí)候用到。NSObject 的實(shí)現(xiàn)并不知道當(dāng)前 class 有哪些成員戚丸,所以它只返回了當(dāng)前對象的名字和地址划址,NSObject
的子類可以覆蓋實(shí)現(xiàn)該方法返回更多信息扔嵌,例如 Foundation
中的NSArray
類返回一個(gè)包含數(shù)組所有元素的列表限府。
? 有些 NSObject
中的方法只是從系統(tǒng) runtime
查信息,例如 isKindOfClass:
和 isMemberOfClass
這兩個(gè)是測試類在繼承層級中的位置; respondsToSelector:
可以讓我們知道某個(gè)對象是否接受特定的消息;conformsToProtocol
確認(rèn)某個(gè)對象是否實(shí)現(xiàn)了特定protocol
中定義的方法痢缎;methodForSelector
可以讓我們拿到函數(shù)的實(shí)現(xiàn)地址胁勺。
Runtime 函數(shù)
系統(tǒng) runtime
是一個(gè)包含一些列方法和數(shù)據(jù)結(jié)構(gòu)的一個(gè)公共接口,它的頭文件在系統(tǒng)目錄/usr/include/objc
. 這里面很多函數(shù)可以讓我們用純 C
做編譯器在編譯 Objective-C
時(shí)候?yàn)槲覀冏龅氖虑槎揽酢F渌A(chǔ)的一些能力都暴露在 NSObject
類中署穗。我們可以用這些函數(shù)在系統(tǒng) runtime 上開發(fā)其他的 interface
和開發(fā)環(huán)境的工具寥裂,在使用 Objective-c
時(shí)候編程時(shí)候一般使用不到
發(fā)消息
這部分主要介紹調(diào)用 oc 方法的表達(dá)式如何被轉(zhuǎn)換到 objc_msgSend
的函數(shù)調(diào)用,如何利用 objec_msgSend
避免動(dòng)態(tài)綁定案疲。
objc_msgSend 函數(shù)
在 Objective-C
只有在運(yùn)行時(shí)封恰,消息才被關(guān)聯(lián)到方法實(shí)現(xiàn)。編譯器轉(zhuǎn)換如下發(fā)消息的表達(dá)式:
? [receiver message]
到 objc_msgSend
的消息函數(shù)調(diào)用褐啡。 objc_msgSend
函數(shù)接受兩個(gè)參數(shù)诺舔,一個(gè)是消息接受者,另外一個(gè)是發(fā)給消息接受者的消息名字备畦,也就是方法名字低飒,或者叫 method selector
, 這是兩個(gè)很重要的參數(shù)懂盐。
? objc_msgSend(receiver, selector)
在發(fā)消息過程中傳遞的其他參數(shù)也會(huì)被轉(zhuǎn)objc_msgSend
函數(shù)處理:
? objc_msgSend(receiver, selector, arg1, arg2, ... )
在動(dòng)態(tài)綁定過程中 objc_msgSend
會(huì)完成所有需要做的事情:
首先找到
selector
指向的方法實(shí)現(xiàn)(函數(shù)指針)褥赊。因?yàn)橥瑯拥姆椒ㄔ诶^承書上可能被不同的類實(shí)現(xiàn),找到最終的方法實(shí)現(xiàn)取決于消息接受者是哪個(gè)類莉恼。然后調(diào)用該方法實(shí)現(xiàn)(函數(shù)指針)拌喉,傳遞接受消息者的對象(指向它的數(shù)據(jù)的指針),同時(shí)改方法需要的其他參數(shù)也會(huì)被一起傳遞俐银。
-
最后司光,返回方法的返回值。
編譯器生成對發(fā)消息函數(shù)的調(diào)用悉患,你不用直接在代碼中直接調(diào)
上面發(fā)消息的關(guān)鍵在編譯器為每個(gè)類和對象構(gòu)建的數(shù)據(jù)結(jié)構(gòu)残家。每個(gè)類的結(jié)構(gòu)包含下面兩個(gè)關(guān)鍵的部分:
- 指向父類的指針
- 類的方法分發(fā)表(dispatch table)。 這個(gè)表 key 是
method selector
value 為selector 對應(yīng)類的方法實(shí)現(xiàn)的地址(函數(shù)指針)售躁。setOrigin::
方法的selector 和setOrigin::
的實(shí)現(xiàn)的地址作為關(guān)聯(lián)的key 和 value坞淮。
當(dāng)一個(gè)新的對象創(chuàng)建的時(shí)候,操作系統(tǒng)分配給它需要的內(nèi)存陪捷,初始化成員變量回窘。在對象的所有成員變量中有一個(gè)指向該對象的類結(jié)構(gòu)體的指針。這個(gè)指針叫 isa
, 通過這個(gè)指針市袖,該對象可以訪問到它的類啡直, 通過它的類可有空訪問到它的繼承書中所有的類。
isa
指針是一個(gè)對象和Objective-C
運(yùn)行時(shí)交互的關(guān)鍵苍碟。一個(gè)對象的結(jié)構(gòu)需要和objc/objc.h
中定義的結(jié)構(gòu)體struct objc_object
一致酒觅。然后,很少我們需要自己創(chuàng)建root 對象微峰, 一般都會(huì)直接繼承NSObject
或者NSProxy
舷丹,他們已經(jīng)自動(dòng)包含了isa
變量
class 和 對象 的結(jié)構(gòu)如下表:
向一個(gè)對象發(fā)消息時(shí)候,消息函數(shù)(objc_msgSend
) 沿著這個(gè)對象的 isa
指針找到該對象的類的結(jié)構(gòu)體蜓肆,在當(dāng)前類結(jié)構(gòu)體的 函數(shù)表(dispatch table)中查找發(fā)送的消息的selector颜凯。如果找不到谋币,消息函數(shù)(objc_msgSend)就繼續(xù)沿著當(dāng)前類中指向父類的指針找到父類的結(jié)構(gòu),在父類的函數(shù)表(dispatch_table) 繼續(xù)查找症概。查找過程會(huì)一直沿著繼承圖一直找到 NSObject
類蕾额。一旦找到對應(yīng)的方法,消息函數(shù)會(huì)直接調(diào)用對應(yīng)的表中方法的函數(shù)實(shí)現(xiàn)彼城,同時(shí)傳給它參數(shù)和對象的數(shù)據(jù)結(jié)構(gòu)凡简。
這就是運(yùn)行時(shí)選擇方法實(shí)現(xiàn)的過程,或者用面向?qū)ο笳Z言的術(shù)語來講:方法動(dòng)態(tài)綁定到消息的過程精肃。
runtime 系統(tǒng)一般會(huì)為使用過的消息的 selector 和 函數(shù)指針做一個(gè)緩存秤涩,加速消息處理的過程,每個(gè)類都有一個(gè)這樣獨(dú)立的緩存司抱,它同時(shí)包含了當(dāng)前類定義的selector 和從父類中繼承的方法筐眷。在搜索類的方法表之前,會(huì)先檢查接受消息者對象的類的緩存(理論上使用過的方法很有可能再次被使用)习柠。如果 selector 在緩存中匀谣,發(fā)消息僅僅比直接進(jìn)行函數(shù)調(diào)用慢一點(diǎn)。只要一個(gè)程序運(yùn)行的時(shí)間足夠長资溃,能夠把所有的方法都緩存起來武翎,幾乎所有消息都能在緩存中找到對應(yīng)的方法。隨著程序的運(yùn)行溶锭,緩存會(huì)動(dòng)態(tài)的增長來容納新消息宝恶。
使用影藏參數(shù)
當(dāng)消息函數(shù)(objc_msgSend
)找到一個(gè)selector 對應(yīng)的方法實(shí)現(xiàn)時(shí)候,調(diào)用該方法時(shí)候時(shí)候回把所有的參數(shù)都傳遞過去趴捅,同時(shí)他也會(huì)傳遞一下兩個(gè)隱藏參數(shù):
- 接受消息的對象(self)
- 方法實(shí)現(xiàn)對應(yīng)的
selector
消息表達(dá)式給每個(gè)方法實(shí)現(xiàn)的參數(shù)都是顯式的參數(shù)信息垫毙。上面兩個(gè)參數(shù)之所以被叫做隱藏參數(shù),是因?yàn)樯厦鎯蓚€(gè)參數(shù)并不在方法定義的源碼中聲明拱绑,上面兩個(gè)參數(shù)在編譯期間被動(dòng)態(tài)的插入综芥。
雖然這兩個(gè)參數(shù)不是顯式聲明,源碼仍然可以方法到他們猎拨。消息的接受者對象就是 self
, 它的 selector 就是 _cmd
膀藐。下面的例子中, _cmd
指向的selector 就是strange
,self
就是接受 strange
消息的對象红省。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
self
是兩個(gè)參數(shù)中最常用的一個(gè)额各,接受消息的對象的成員變量也可以在定義的方法中訪問到。
獲得方法地址
避免動(dòng)態(tài)綁定的唯一方法是獲取到方法的地址类腮,直接調(diào)用(如果是一個(gè)函數(shù))臊泰。當(dāng)一個(gè)特定的方法被非常多次數(shù)調(diào)用的時(shí)候,而且你想避免前期的發(fā)消息查找的過程時(shí)候蚜枢,這種場景可能比較適合缸逃。
通過 NSObject
類中定義的方法methodForSelector:
, 你可以獲得selector
對應(yīng)的函數(shù)指針,然后直接使用指針調(diào)用函數(shù)厂抽。methodForSelector:
返回的指針必須被轉(zhuǎn)到正確的函數(shù)原型需频,轉(zhuǎn)換過程中也應(yīng)該包含返回值和入?yún)㈩愋汀?/p>
下面的列子展示了setFilled:
放的實(shí)現(xiàn)和調(diào)用:
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);
}
傳遞給函數(shù)實(shí)現(xiàn)的前面兩個(gè)參數(shù)是接受消息的對象(self
) 和 方法selector
(_cmd
)。這兩個(gè)參數(shù)是方法語法中的隱藏參數(shù)筷凤,但是在直接調(diào)用函數(shù)的時(shí)候必須顯示的被傳遞昭殉。
使用 methodForSelector:
避免動(dòng)態(tài)綁定可以節(jié)約發(fā)消息過程中需要的大多時(shí)間。然而藐守,這個(gè)節(jié)約只有特定消息被重復(fù)發(fā)送多次的時(shí)候挪丢,比如上面的 for
循環(huán)中調(diào)用, 類似的場景才會(huì)有意義一些卢厂。
注意 methodForSelector:
方法是 Cocoa runtime 系統(tǒng)提供的乾蓬, 不是 Objective-C
自己的一個(gè)特性。
動(dòng)態(tài)方法解析
這個(gè)部分將會(huì)介紹如何動(dòng)態(tài)提供一個(gè)方法實(shí)現(xiàn)慎恒。
動(dòng)態(tài)方法解析
有一些場景我們需要?jiǎng)討B(tài)提供方法實(shí)現(xiàn)任内,比如,Objective-C
聲明屬性的時(shí)候有一個(gè)特性包含 @dynamic
指令
@dynamic propertyName;
這個(gè)聲明告訴編譯器融柬,跟這個(gè)屬性相關(guān)的方法將會(huì)被動(dòng)態(tài)提供死嗦。
你可以實(shí)現(xiàn) resolvInstanceMethod:
和 resolveClassMethod:
給特定對象selector
和類方法解析。
Objective-C
方法是一個(gè)至少接受兩個(gè)參數(shù)的 C 函數(shù)粒氧,這兩個(gè)參數(shù)分別是 self
和 _cmd
. 我們可以使用 class_addMethod
給一個(gè)類添加一個(gè)函數(shù):
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ...
}
你可以使用 resolveInstanceMethod:
動(dòng)態(tài)的將它添加為方法(方法名字為resolveThisMethodDynamically
)越除,例如:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, @"v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
類在轉(zhuǎn)發(fā)消息機(jī)制介入之前有機(jī)會(huì)動(dòng)態(tài)解析一個(gè)方法。 如果 respondsToSelector:
或者instancesRespondsToSelector:
調(diào)用時(shí)候外盯, 動(dòng)態(tài)方法解析有機(jī)會(huì)給一個(gè)selector 提供一個(gè)函數(shù)實(shí)現(xiàn)(IMP)廊敌。如果你實(shí)現(xiàn) resolveInstanceMethod:
,可以通過在方法中return NO
來實(shí)現(xiàn)只有特定的selector
通過轉(zhuǎn)發(fā)機(jī)制被轉(zhuǎn)發(fā)门怪。
動(dòng)態(tài)加載
Objective-C 程序可以在程序運(yùn)行時(shí)動(dòng)態(tài)加載和鏈接新類和擴(kuò)展(category)骡澈。新代碼被納入程序,跟程序啟動(dòng)時(shí)候加載的類和擴(kuò)展(category)一樣對待掷空。
利用動(dòng)態(tài)加載可以做很多事情肋殴。比如,系統(tǒng)偏好應(yīng)用中不同的模塊 就是動(dòng)態(tài)加載的坦弟。
在 Cocoa 環(huán)境中护锤,動(dòng)態(tài)加載通常被用來允許應(yīng)用自定義。其他人可以寫一些你的程序在運(yùn)行期加載的模塊酿傍。加載的模塊擴(kuò)展程序功能烙懦。其他人用你允許的方式貢獻(xiàn)你不期望自己定義的一些功能。你提供框架赤炒,其他人提供具體的實(shí)現(xiàn)氯析。
雖然運(yùn)行時(shí)函數(shù)也有通過 Mach-O 文件執(zhí)行動(dòng)態(tài)加載 Objective-C 模塊的能力(objc_loadModules亏较,在objc/objc-load.h), NSBundle
提供了更方便的動(dòng)態(tài)加載的接口掩缓。
消息轉(zhuǎn)發(fā)
發(fā)消息給一個(gè)不能處理該消息的對象是會(huì)產(chǎn)生錯(cuò)誤雪情。但是在產(chǎn)生錯(cuò)誤結(jié)果之前,系統(tǒng) runtime
會(huì)再次給當(dāng)前對象機(jī)會(huì)處理該消息你辣。
轉(zhuǎn)發(fā)
如果發(fā)消息給一個(gè)不能處理該消息的對象巡通,在通知錯(cuò)誤結(jié)果之前,runtime
發(fā)送 forwardInvocation:
給該對象舍哄,同時(shí)會(huì)帶上一個(gè) NSInvocation
對象作為參數(shù)宴凉。這個(gè)NSInvocation
對象包含了原來消息和參數(shù)。
你可以實(shí)現(xiàn) forwardInvocation:
方法表悬,給消息提供一個(gè)默認(rèn)的響應(yīng)弥锄,或者用其他的方式來避免這個(gè)錯(cuò)誤。就像這個(gè)方法名字暗含的意義签孔,forwardInvocation:
通常用來轉(zhuǎn)發(fā)消息給另外一個(gè)對象叉讥。
為了看到轉(zhuǎn)發(fā)的范圍和意圖,假設(shè)如下場景:首先你正在設(shè)計(jì)一個(gè)可以相應(yīng) negotiate
詳細(xì)的對象饥追,而且你想讓它的相應(yīng)包含其他對象的相應(yīng)图仓。你可以通過傳遞 negotiate
消息給其他對象很容易完成這個(gè)。
進(jìn)一步但绕,假設(shè)你想讓你的對象對 negotiate
的相應(yīng)是在其他類中實(shí)現(xiàn)的相應(yīng)救崔。一個(gè)方法是通過讓你的類繼承其他類。然而,你的類和實(shí)現(xiàn)negotiate
方法的類在不同繼承樹中(OC 不支持多繼承)捏顺。
及時(shí)你的類不能繼承 negotiate
方法六孵,你仍然可以通過實(shí)現(xiàn)一個(gè)傳遞消息給其他類對象的方法:
- (id)negotiate
{
if ([someOtherObject respondsTo:@selector(negotiate)]) {
return [someOtherObject negotiate];
}
return self;
}
這樣的實(shí)現(xiàn)方式可能有些笨重,尤其是你想轉(zhuǎn)發(fā)很多消息給其他對象的時(shí)候幅骄。必須實(shí)現(xiàn)一個(gè)覆蓋每一個(gè)你想轉(zhuǎn)發(fā)到其他對象的方法劫窒。
forwardInvocation:
消息提供的第二次處理消息的機(jī)制。它的機(jī)制是當(dāng)一個(gè)對象無法處理某一個(gè)消息時(shí)候拆座,runtime 會(huì)通過發(fā)送forwardInvocation:
消息通知這個(gè)對象主巍。每一個(gè)類都從 NSObject
繼承了forwardInvocation:
這個(gè)方法。然而 NSObject
實(shí)現(xiàn)這個(gè)方法僅僅是調(diào)用 doesNotRecognizeSelector:
. 通過覆蓋NSObject
的實(shí)現(xiàn)挪凑,你可以利用 forwardInvocation:
這個(gè)消息的機(jī)會(huì)轉(zhuǎn)發(fā)消息到其他對象孕索。
要轉(zhuǎn)發(fā)一個(gè)消息,所有的 forwardMethodInvocation:
方法需要:
- 決定這個(gè)消息應(yīng)該被轉(zhuǎn)到哪里去
- 發(fā)送消息并帶上原來的參數(shù)
發(fā)送消息可以通過 invokeWithTarget:
方法:
- (void)forwardInvocation:(NSInvocation*)anInvocation
{
if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:someOtherObject];
} else {
[super forwardInvocation:anInvocation];
}
}
被轉(zhuǎn)發(fā)的消息的返回值會(huì)被返回到原來的發(fā)送者躏碳。所有類型的返回值都可以被返回給發(fā)送者搞旭,包括 id,結(jié)構(gòu)體,單雙進(jìn)度的浮點(diǎn)數(shù)肄渗。
forwardInvocation:
方法看起來像不能處理的消息的分發(fā)中心镇眷,打包不能處理的消息到其他不同的接受者】疑叮或者說是一個(gè)轉(zhuǎn)送站偏灿,發(fā)送所有的消息到同一個(gè)地方丹诀。它可以把一個(gè)消息轉(zhuǎn)換到另外一個(gè)消息钝的, 也可以直接吞掉某些消息,因此被吞掉的消息既沒有錯(cuò)誤頁沒有響應(yīng)铆遭,forwardInvocation:
方法也可以對多個(gè)消息發(fā)送一個(gè)響應(yīng)硝桩,forwardInvocation:
能做什么還得看具體的實(shí)現(xiàn)細(xì)節(jié)。
forwardInvocation:
方法只處理那些對對象發(fā)送不存在的消息或者方法枚荣。例如碗脊,你想讓你的類可以轉(zhuǎn)發(fā)negotiate
消息到其他類,那就是說你的類不能有negotiate
方法橄妆,否則的話衙伶,消息不會(huì)到達(dá)forwardInvocation:
。
轉(zhuǎn)發(fā)和多繼承
消息轉(zhuǎn)發(fā)機(jī)制也模仿了繼承機(jī)制害碾,它也可以讓Objective-C
程序具有多繼承
特性矢劲,如下圖所示,一個(gè)對象對一個(gè)消息的響應(yīng)過程似乎是通過在“繼承”樹中其他的類中去查找方法實(shí)現(xiàn)慌随。
Warrior
class 的對象實(shí)例轉(zhuǎn)發(fā)negotiate
消息給Diplomat
類的實(shí)例芬沉。 Warrior
類的實(shí)例看起來像跟Diplomat
類的實(shí)例一樣可以響應(yīng) negotiate
消息(雖然實(shí)際是Diplomat
實(shí)例響應(yīng)的消息)。
從繼承樹的兩個(gè)分支(它自己的繼承分支和轉(zhuǎn)到到的對象的繼承分支)轉(zhuǎn)發(fā)消息阁猜。上面的例子丸逸,看起來是 Warrior
類繼承了Diplomat
類。
轉(zhuǎn)發(fā)機(jī)制提供了多繼承的可能性剃袍。然而這兩個(gè)之間有一個(gè)很重要的區(qū)別:多繼承會(huì)結(jié)合不同的能力到一個(gè)類黄刚,而轉(zhuǎn)發(fā)只是分配不同的相應(yīng)到其他對象,把一個(gè)大問題拆分為幾個(gè)小的問題民效,用一種方式關(guān)聯(lián)起來憔维,對消息發(fā)送者透明。
轉(zhuǎn)發(fā)和繼承
雖然轉(zhuǎn)發(fā)看起來有點(diǎn)像繼承研铆,但是 NSObject
類對他們區(qū)分的卻很明顯埋同。 respondsToSelector:
和isKindOfClass:
這個(gè)兩個(gè)方法只查找繼承樹,并不會(huì)在轉(zhuǎn)發(fā)鏈上查找棵红。例如 Warrior
對象如下調(diào)用:
if ([aWarrior respondsToSelector:@selector(negotiate)]) {
...
}
結(jié)果是 NO
, 即使它可以接受 negotiate
消息凶赁,返回正確的結(jié)果,中間沒出任何錯(cuò)誤。