Objective-C Runtime 之消息傳遞

致敬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庫主要做下面幾件事:

  1. 封裝:在這個庫中诱担,對象可以用C語言中的結(jié)構(gòu)體表示毡证,而方法可以用C函數(shù)來實現(xiàn),另外再加上了一些額外的特性蔫仙。這些結(jié)構(gòu)體和函數(shù)被runtime函數(shù)封裝后料睛,我們就可以在程序運行時創(chuàng)建,檢查摇邦,修改類恤煞、對象和它們的方法了。

  2. 找出方法的最終執(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ī)制基本上分為三個步驟:

  1. 動態(tài)方法解析
  2. 備用接收者
  3. 完整轉(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ù):

  1. 定位可以響應(yīng)封裝在anInvocation中的消息的對象唯卖。這個對象不需要能處理所有未知消息。
  2. 使用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ā)流程如下:


消息轉(zhuǎn)發(fā)示意圖
相關(guān)參考

Objective-C Runtime
Objective-C Runtime 運行時之一:類與對象
重識 Objective-C Runtime - Smalltalk 與 C 的融合

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玷室,一起剝皮案震驚了整個濱河市零蓉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌穷缤,老刑警劉巖敌蜂,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異津肛,居然都是意外死亡章喉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門身坐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秸脱,“玉大人,你說我怎么就攤上這事部蛇√剑” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵涯鲁,是天一觀的道長巷查。 經(jīng)常有香客問我,道長撮竿,這世上最難降的妖魔是什么吮便? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮幢踏,結(jié)果婚禮上髓需,老公的妹妹穿的比我還像新娘。我一直安慰自己房蝉,他們只是感情好僚匆,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搭幻,像睡著了一般咧擂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上檀蹋,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天松申,我揣著相機(jī)與錄音,去河邊找鬼俯逾。 笑死贸桶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的桌肴。 我是一名探鬼主播皇筛,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坠七!你這毒婦竟也來了水醋?” 一聲冷哼從身側(cè)響起旗笔,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拄踪,沒想到半個月后蝇恶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡惶桐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年艘包,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耀盗。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖卦尊,靈堂內(nèi)的尸體忽然破棺而出叛拷,到底是詐尸還是另有隱情,我是刑警寧澤岂却,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布忿薇,位于F島的核電站,受9級特大地震影響躏哩,放射性物質(zhì)發(fā)生泄漏署浩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一扫尺、第九天 我趴在偏房一處隱蔽的房頂上張望筋栋。 院中可真熱鬧,春花似錦正驻、人聲如沸弊攘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽襟交。三九已至,卻和暖如春伤靠,著一層夾襖步出監(jiān)牢的瞬間捣域,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工宴合, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留焕梅,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓形纺,卻偏偏與公主長得像丘侠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子逐样,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容