致敬Smalltalk
時至今日,Smalltalk已是昨日黃花,若不是Objective-C ,可能這里你我都不一定有機(jī)會提及這門語言.但如果我們能夠重回1980年, 回望整個計算機(jī)編程語言領(lǐng)域, 特別是工業(yè)界編程, 打死也不會想到日后 Java 這種無名小卒, 以及 C++ 這個又面向?qū)ο笥种С诌^程的雙面間諜能夠紅得發(fā)紫. 當(dāng)年最流行的語言, 當(dāng)屬 FORTRAN, C 和 Smalltalk悼做。
Smalltalk 是世界上第二個面向?qū)ο蟮恼Z言贰逾。(那第一個面向?qū)ο蟮恼Z言是什么呢?據(jù)說是Simula 67,來自百度).更多關(guān)于Smalltalk可以閱讀這篇文章編程珠璣番外篇-8.Smalltalk 中的珠璣,其中消息傳遞是Smalltalk的留給后世晶亮的珠璣饲宛。
關(guān)于Objective-C
Objective-C從名字就可以看出來和C語言有著千絲萬縷的聯(lián)系, 它擴(kuò)展了C語言,加入了面向?qū)ο蟮奶匦院蚐malltalk式的消息傳遞機(jī)制秧倾。
Objective-C是一門動態(tài)語言嚷量,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了運行時來處理俩功。C語言中 ,調(diào)用一個方法其實就是跳到內(nèi)存中的某一點并開始執(zhí)行一段代碼蒋川。沒有任何動態(tài)的特性势誊,因為這在編譯時就決定好了。而在 Objective-C 中唆貌,[object foo]
語法并不會立即執(zhí)行 foo 這個方法的代碼滑潘。它是在運行時給 object 發(fā)送一條叫 foo 的消息。這個消息锨咙,也許會由 object 來處理语卤,也許會被轉(zhuǎn)發(fā)給另一個對象,或者不予理睬假裝沒收到這個消息。多條不同的消息也可以對應(yīng)同一個方法實現(xiàn)粹舵。這些都是在程序運行的時候決定的钮孵。
這種特性意味著Objective-C不僅需要一個編譯器,還需要一個運行時系統(tǒng)來執(zhí)行編譯的代碼齐婴。對于Objective-C來說油猫,這個運行時系統(tǒng)就像一個操作系統(tǒng)一樣:它讓所有的工作可以正常的運行。這個運行時系統(tǒng)即Objc Runtime
柠偶。Objc Runtime
其實是一個Runtime
庫情妖,它基本上是用C和匯編寫的,這個庫使得C語言有了面向?qū)ο蟮哪芰Α?/p>
Runtime
庫主要做下面幾件事:
封裝:在這個庫中诱担,對象可以用C語言中的結(jié)構(gòu)體表示毡证,而方法可以用C函數(shù)來實現(xiàn),另外再加上了一些額外的特性蔫仙。這些結(jié)構(gòu)體和函數(shù)被runtime函數(shù)封裝后料睛,我們就可以在程序運行時創(chuàng)建,檢查摇邦,修改類恤煞、對象和它們的方法了。
找出方法的最終執(zhí)行代碼:當(dāng)程序執(zhí)行
[object doSomething]
時施籍,會向消息接收者(object)發(fā)送一條消息(doSomething)居扒,runtime會根據(jù)消息接收者是否能響應(yīng)該消息而做出不同的反應(yīng)。
可以說最初的 Objective-C = C + Preprocessor + Runtime丑慎。Runtime 是Objective-C面向?qū)ο蠛蛣討B(tài)特性的基石.
消息發(fā)送
在 Objective-C 中喜喂,類、對象和方法都是一個 C 的結(jié)構(gòu)體竿裂,從 objc/objc.h
頭文件中玉吁,我們可以找到他們的定義:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
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;
#endif
};
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
objc_method_list
本質(zhì)是一個有 objc_method
元素的可變長度的數(shù)組。一個 objc_method
結(jié)構(gòu)體中有函數(shù)名腻异,也就是SEL进副,有表示函數(shù)類型的字符串 (見 Type Encoding) ,以及函數(shù)的實現(xiàn)IMP悔常。
從這些定義中可以看出發(fā)送一條消息也就 objc_msgSend 做了什么事敢会。
當(dāng)消息發(fā)送給一個對象時,objc_msgSend通過對象的isa指針獲取到類的結(jié)構(gòu)體这嚣,然后在方法分發(fā)表里面查找方法的selector。如果沒有找到selector塞俱,則通過objc_msgSend結(jié)構(gòu)體中的指向父類的指針找到其父類姐帚,并在父類的分發(fā)表里面查找方法的selector。依此障涯,會一直沿著類的繼承體系到達(dá)NSObject類罐旗。一旦定位到selector膳汪,函數(shù)會就獲取到了實現(xiàn)的入口點,并傳入相應(yīng)的參數(shù)來執(zhí)行方法的具體實現(xiàn)九秀。如果最后沒有定位到selector遗嗽,則會走消息轉(zhuǎn)發(fā)流程。
消息傳遞示意圖
舉 objc_msgSend(obj, foo) 這個例子來說:
1.首先鼓蜒,通過 obj 的 isa 指針找到它的 class ;
2.在 class 的 method list 找 foo ;
3.如果 class 中沒到 foo痹换,繼續(xù)往它的 superclass 中找 ;
4.一旦找到 foo 這個函數(shù),就去執(zhí)行它的實現(xiàn)IMP .
但這種實現(xiàn)有個問題都弹,效率低娇豫。但一個 class 往往只有 20% 的函數(shù)會被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的 80% 畅厢。每個消息都需要遍歷一次 objc_method_list 并不合理冯痢。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來,那可以大大提高函數(shù)查詢的效率框杜。這也就是 objc_class 中另一個重要成員 objc_cache 做的事情 - 再找到 foo 之后浦楣,把 foo 的 method_name 作為 key ,method_imp 作為 value 給存起來咪辱。當(dāng)再次收到 foo 消息的時候振劳,可以直接在 cache 里找到,避免去遍歷 objc_method_list.
消息轉(zhuǎn)發(fā)
當(dāng)一個對象能接收一個消息時梧乘,就會走正常的方法調(diào)用流程澎迎。但如果一個對象無法接收指定消息時,又會發(fā)生什么事呢选调?默認(rèn)情況下夹供,如果是以[object message]
的方式調(diào)用方法,如果object
無法響應(yīng)message
消息時仁堪,編譯器會報錯哮洽。但如果是以perform...
的形式來調(diào)用,則需要等到運行時才能確定object是否能接收message
消息弦聂。如果不能鸟辅,則程序崩潰。
通常莺葫,當(dāng)我們不能確定一個對象是否能接收某個消息時匪凉,會先調(diào)用respondsToSelector:
來判斷一下。如下代碼所示:
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
不過捺檬,我們這邊想討論下不使用respondsToSelector:
判斷的情況再层。這才是我們這一節(jié)的重點。
當(dāng)一個對象無法接收某一消息時,就會啟動所謂”消息轉(zhuǎn)發(fā)(message forwarding)“機(jī)制聂受,通過這一機(jī)制蒿秦,我們可以告訴對象如何處理未知的消息。默認(rèn)情況下蛋济,對象接收到未知的消息棍鳖,會導(dǎo)致程序崩潰,通過控制臺碗旅,我們可以看到以下異常信息:
-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance***
這段異常信息實際上是由NSObject的”doesNotRecognizeSelector
“方法拋出的渡处。不過,我們可以采取一些措施扛芽,讓我們的程序執(zhí)行特定的邏輯骂蓖,而避免程序的崩潰。
消息轉(zhuǎn)發(fā)機(jī)制基本上分為三個步驟:
- 動態(tài)方法解析
- 備用接收者
- 完整轉(zhuǎn)發(fā)
下面我們詳細(xì)討論一下這三個步驟川尖。
1.動態(tài)方法解析
對象在接收到未知的消息時登下,首先會調(diào)用所屬類的類方法+resolveInstanceMethod:
(實例方法)或者+resolveClassMethod:
(類方法)。在這個方法中叮喳,我們有機(jī)會為該未知消息新增一個”處理方法””被芳。不過使用該方法的前提是我們已經(jīng)實現(xiàn)了該”處理方法”,只需要在運行時通過class_addMethod
函數(shù)動態(tài)添加到類里面就可以了馍悟。如下代碼所示:
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
不過這種方案更多的是為了實現(xiàn)@dynamic
屬性畔濒。
2.備用接收者
如果在上一步無法處理消息,則Runtime會繼續(xù)調(diào)以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一個對象實現(xiàn)了這個方法锣咒,并返回一個非nil的結(jié)果侵状,則這個對象會作為消息的新接收者,且消息會被分發(fā)到這個對象毅整。當(dāng)然這個對象不能是self
自身趣兄,否則就是出現(xiàn)無限循環(huán)。當(dāng)然悼嫉,如果我們沒有指定相應(yīng)的對象來處理aSelector
艇潭,則應(yīng)該調(diào)用父類的實現(xiàn)來返回結(jié)果。
使用這個方法通常是在對象內(nèi)部戏蔑,可能還有一系列其它對象能處理該消息蹋凝,我們便可借這些對象來處理消息并返回,這樣在對象外部看來总棵,還是由該對象親自處理了這一消息鳍寂。如下代碼所示:
@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[SUTRuntimeMethodHelper alloc] init];
}
return self;
}
- (void)test {
[self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 將消息轉(zhuǎn)發(fā)給_helper來處理
if ([selectorString isEqualToString:@"method2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
這一步合適于我們只想將消息轉(zhuǎn)發(fā)到另一個能處理該消息的對象上。但這一步無法對消息進(jìn)行處理情龄,如操作消息的參數(shù)和返回值伐割。
3.完整消息轉(zhuǎn)發(fā)
如果在上一步還不能處理未知消息候味,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。此時會調(diào)用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
運行時系統(tǒng)會在這一步給消息接收者最后一次機(jī)會將消息轉(zhuǎn)發(fā)給其它對象隔心。對象會創(chuàng)建一個表示消息的NSInvocation
對象,把與尚未處理的消息有關(guān)的全部細(xì)節(jié)都封裝在anInvocation
中尚胞,包括selector
硬霍,目標(biāo)(target
)和參數(shù)。我們可以在forwardInvocation
方法中選擇將消息轉(zhuǎn)發(fā)給其它對象笼裳。
forwardInvocation:
方法的實現(xiàn)有兩個任務(wù):
- 定位可以響應(yīng)封裝在
anInvocation
中的消息的對象唯卖。這個對象不需要能處理所有未知消息。 - 使用
anInvocation
作為參數(shù)躬柬,將消息發(fā)送到選中的對象拜轨。anInvocation
將會保留調(diào)用結(jié)果,運行時系統(tǒng)會提取這一結(jié)果并將其發(fā)送到消息的原始發(fā)送者允青。
不過橄碾,在這個方法中我們可以實現(xiàn)一些更復(fù)雜的功能,我們可以對消息的內(nèi)容進(jìn)行修改颠锉,比如追回一個參數(shù)等法牲,然后再去觸發(fā)消息。另外琼掠,若發(fā)現(xiàn)某個消息不應(yīng)由本類處理拒垃,則應(yīng)調(diào)用父類的同名方法,以便繼承體系中的每個類都有機(jī)會處理此調(diào)用請求瓷蛙。
還有一個很重要的問題悼瓮,我們必須重寫以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
消息轉(zhuǎn)發(fā)機(jī)制使用從這個方法中獲取的信息來創(chuàng)建NSInvocation
對象。因此我們必須重寫這個方法艰猬,為給定的selector
提供一個合適的方法簽名横堡。
完整的示例如下所示:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}
NSObject的forwardInvocation:
方法實現(xiàn)只是簡單調(diào)用了doesNotRecognizeSelector:
方法,它不會轉(zhuǎn)發(fā)任何消息姥宝。這樣翅萤,如果不在以上所述的三個步驟中處理未知消息,則會引發(fā)一個異常腊满。
從某種意義上來講套么,forwardInvocation:
就像一個未知消息的分發(fā)中心,將這些未知的消息轉(zhuǎn)發(fā)給其它對象碳蛋∨呙冢或者也可以像一個運輸站一樣將所有未知消息都發(fā)送給同一個接收對象。這取決于具體的實現(xiàn)肃弟。
一圖勝千言,消息轉(zhuǎn)發(fā)流程如下:
相關(guān)參考
Objective-C Runtime
Objective-C Runtime 運行時之一:類與對象
重識 Objective-C Runtime - Smalltalk 與 C 的融合