Objective-C Runtime(一): 初探

Objective-C 編譯器與運(yùn)行時(shí)系統(tǒng)支撐著OC程序的運(yùn)行励堡。

Objective-C程序在三個(gè)層面上與runtime系統(tǒng)交互:

  • Objective-C源代碼:編譯器把OC代碼類兼都、方法昆著、成員變量等信息轉(zhuǎn)化為支持語言動(dòng)態(tài)特性的數(shù)據(jù)結(jié)構(gòu)與函數(shù)诫欠。比如消息傳遞機(jī)制中的核心函數(shù)objc_msgSend潦刃,即由OC代碼的消息傳遞語句轉(zhuǎn)換而來变隔。

  • NSObject提供了一系列的自省(Introspection)方法喘落,也是運(yùn)行時(shí)的一部分茵宪。

  • Runtime函數(shù)。

消息傳遞機(jī)制

在Objective-C里瘦棋,消息(message)是到運(yùn)行時(shí)才綁定到方法實(shí)現(xiàn)的.
意思就是說, 像

[receiver message];

這樣一條語句, 編譯器會(huì)把他轉(zhuǎn)換為

objc_msgSend(receiver, selector);

這樣第一個(gè)C語言的函數(shù)調(diào)用, 參數(shù)分別是消息接受者(對(duì)象), 消息對(duì)應(yīng)的方法名稱(選擇子), 若改方法帶參數(shù), 則為

objc_msgSend(receiver, selector, arg1, arg2, ...)

該函數(shù)的動(dòng)態(tài)綁定過程是這樣的:

  • 它首先沿著類的繼承體系去尋找選擇子對(duì)應(yīng)的方法實(shí)現(xiàn).
  • 找到后調(diào)用具體的方法實(shí)現(xiàn), 并把對(duì)象指針以及各參數(shù)傳遞給該方法, 隨后調(diào)用它.
  • 最后返回該方法的返回值.

它的函數(shù)原型:

id objc_msgSend(id self, SEL cmd, ...)

回頭看動(dòng)態(tài)綁定過程的第一步.
Objective-C里, 每個(gè)類里都維護(hù)著一張表格(dispatch table), 其中的指針正是指向該類下所定義的方法實(shí)現(xiàn), 而方法的選擇子(selector)作為查表用的"鍵".
每個(gè)類里除了該表之外, 還擁有一個(gè)指向其父類的指針.

這些類與對(duì)象的結(jié)構(gòu)是這樣的:

對(duì)象實(shí)例里有一個(gè)isa指針, 指向它的類對(duì)象.

objc_msgSend函數(shù)依賴著上述的繼承體系去查找并調(diào)用恰當(dāng)?shù)姆椒?

為了加速方法的查找, 每個(gè)類里除了自身定義的方法列表外, 還維護(hù)這一張快速映射表作為緩存. 多次對(duì)它查找同一selector將不再向上追溯查找, 而直接查找本身的緩存并返回對(duì)應(yīng)的方法實(shí)現(xiàn).

剛才提到要調(diào)用的方法實(shí)現(xiàn), 每個(gè)OC對(duì)象的方法都可視為一個(gè)C函數(shù), 其原型如下:

<return_type> Class_selector(id self, SEL _cmd, ...)

實(shí)際函數(shù)名可能跟上面的不一樣. 但注意的是該函數(shù)里是包括了self_cmd兩個(gè)隱含參數(shù)的. 所謂"隱含", 是指在開發(fā)人員編寫的方法代碼里, 是不存在這兩個(gè)參數(shù), 但我們都可以通過這兩個(gè)變量名去訪問.

消息轉(zhuǎn)發(fā)機(jī)制

在上一節(jié)消息傳遞機(jī)制中, 對(duì)象接收到一個(gè)消息后, 去搜尋其對(duì)應(yīng)方法實(shí)現(xiàn)的函數(shù)地址. 若搜尋不到, 并不馬上拋出異常, 而是再給接受者一次機(jī)會(huì), 進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制.

消息轉(zhuǎn)發(fā)分為兩大階段. 第一階段先征詢接受者所屬的類, 看其是否能動(dòng)態(tài)添加方法, 以處理當(dāng)前這個(gè)未知的選擇子(unknown selector), 這叫做動(dòng)態(tài)方法解析(Dynamic Method Resolution); 第二階段則為"完整的消息轉(zhuǎn)發(fā)機(jī)制"(full forwarding mechanism).

動(dòng)態(tài)方法解析

對(duì)象在收到無法解讀的消息后, 首先將調(diào)用其所屬類的下列類方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

顧名思義, 該方法作用為解析實(shí)例方法, 相應(yīng)地也有個(gè)類似的方法, 為解析類方法所用: resolveClassMethod.
此方法在respondsToSelector:instancesRespondToSelector:被調(diào)用后返回前, 也有一次機(jī)會(huì)為自己動(dòng)態(tài)添加一個(gè)方法的實(shí)現(xiàn).

動(dòng)態(tài)方法解析常用來實(shí)現(xiàn) @dynamic 屬性.

下面看一個(gè)完整的例子演示動(dòng)態(tài)方法解析.

假設(shè)要編寫一個(gè)類似"字典"的對(duì)象, 它里面可以容納其它對(duì)象, 只不過開發(fā)者要直接通過屬性來存取其中的數(shù)據(jù). 這個(gè)類的設(shè)計(jì)思路是: 有開發(fā)者來添加屬性定義, 并將其聲明為 @dynamic, 類則會(huì)自動(dòng)處理相關(guān)屬性值的存放與獲取操作. 聽起來不錯(cuò)吧? >_<

類的接口定義如下:

#import <Foundation/Foundation.h>

@interface AutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end

這個(gè)類將裝載各種不同類型的對(duì)象, 看起來與平時(shí)普通的類沒啥區(qū)別啊? 我們看類的實(shí)現(xiàn).

#import "AutoDictionary.h"
#import <objc/runtime.h>

@interface AutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation AutoDictionary

@dynamic string, number, date, opaqueObject;

- (instancetype)init {
    if (self = [super init]) {
        _backingStore = [[NSMutableDictionary alloc] init];
    }
    return self;
}

聲明各屬性為 @dynamic, 編譯器不會(huì)自動(dòng)為property生成存取方法和實(shí)例變量. 由我們自行實(shí)現(xiàn).

關(guān)鍵在于resolveInstanceMethod:方法的實(shí)現(xiàn).

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    
    return [super resolveInstanceMethod:sel];
}

眾所周知, 任何的點(diǎn)語法訪問都會(huì)轉(zhuǎn)化為名為<name>, set<Name>形式的存取方法來訪問, 以上使用class_addMethod在運(yùn)行時(shí)添加存取方法, 所有屬性將共用這一對(duì)getter與setter.

class_addMethod函數(shù)第一和第二參數(shù)分別為類對(duì)象自身與選擇子, 第三個(gè)參數(shù)為待添加方法實(shí)現(xiàn)對(duì)應(yīng)的函數(shù)指針, 第四為待添加方法的"類型編碼", 指定該添加方法的參數(shù)與返回值等.

使用class_addMethod動(dòng)態(tài)添加方法后, 所添加的方法將一直在運(yùn)行時(shí)存在, 下一次的調(diào)用該方法將不再進(jìn)行動(dòng)態(tài)方法解析.

下面實(shí)現(xiàn)getter與setter:

// getter
id autoDictionaryGetter(id self, SEL _cmd) {
    // Get the backing store from the object
    AutoDictionary *typedSelf = (AutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    // The key is simply the selector name
    NSString *key = NSStringFromSelector(_cmd);
    
    // Return the value
    return [backingStore objectForKey:key];
    
}

//setter
void autoDictionarySetter(id self, SEL _cmd, id value) {
    // Get the backing store from the object
    AutoDictionary *typedSelf = (AutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    /** The selector will be for example, "setOpaqueObject:".
     *  We need to remove the "set", ":" and lowercase the first
     *  letter of the remainder.
     */
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    // Remove the `:' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    // Remove the `set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // Lowercase the first character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

使用它們的方式很簡單:

AutoDictionary *dict = [[AutoDictionary alloc] init];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);
//Output: dict.date = 1985-01-24 00:00:00 +0000

而且它還是KVC兼容的哦! *(關(guān)于KVC與KVO, 可參考我之前的博客

[dict setValue:@"I'm a string!" forKey:@"string"];
NSLog(@"dict.string = %@", dict.string);
//Output: dict.string = I'm a string!

備援接受者

在完整的消息轉(zhuǎn)發(fā)來臨之前, 當(dāng)前接受者還有第二次機(jī)會(huì)處理未知的選擇子. 處理方法如下:

- (id)forwardingTargetForSelector:(SEL)aSelector

運(yùn)行時(shí)系統(tǒng)通過該方法詢問能否把無法識(shí)別的選擇子轉(zhuǎn)給其它對(duì)象處理呢?
例如, 在一個(gè)對(duì)象內(nèi)部, 可能還有其它一系列對(duì)象, 該對(duì)象可經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對(duì)象返回. 這樣看來, 就好像是該對(duì)象親自處理了這些消息似的. 這樣可以模擬出"多重繼承"的特性.

完整的消息轉(zhuǎn)發(fā)

終于來到了這一步. 首先創(chuàng)建NSInvocation對(duì)象, 把尚未處理的有關(guān)該消息的全部細(xì)節(jié)封裝起來, 包括選擇子, 目標(biāo)(target), 參數(shù)與返回值等. 在觸發(fā)NSInvocation對(duì)象時(shí), 消息派發(fā)系統(tǒng)(message-dispatch system)將親自出馬, 把消息指派給目標(biāo)對(duì)象.

消息轉(zhuǎn)發(fā)方法:

- (void)forwardInvocation:(NSInvocation *)invocation

在此方法里需要做的事情是:

  • 決定消息發(fā)送的目標(biāo)對(duì)象;
  • 隨參數(shù)一起發(fā)送該消息.

消息通過invokeWithTarget:發(fā)送.

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

以上代碼中最后調(diào)用超類處理該消息, 沿著繼承體系向上, 每個(gè)類都有機(jī)會(huì)處理該請(qǐng)求, 直至NSObject, 它的該方法默認(rèn)實(shí)現(xiàn)為拋出doesNotRecognizeSelector:異常.

相對(duì)于簡單的消息發(fā)送語句 [receiver message];, forwardInvocation:提供了一種更加靈活的機(jī)制, 避免了冗余的方法重寫或者破壞類繼承體系, 而提供了一種類似"消息中轉(zhuǎn)派發(fā)"的機(jī)制. 另外NSInvocation也提供了對(duì)待轉(zhuǎn)發(fā)消息的修改機(jī)制, 甚至不做轉(zhuǎn)發(fā), 等等, 也提供了更多的操作性.

初探Objective-C Runtime System, 這篇博文對(duì)Runtime消息傳遞, 轉(zhuǎn)發(fā)機(jī)制等做了一些探討. 關(guān)于更多的Runtime研究與實(shí)踐, 將在日后的博客中更新.

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稀火,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子赌朋,更是在濱河造成了極大的恐慌凰狞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沛慢,死亡現(xiàn)場(chǎng)離奇詭異赡若,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)团甲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門斩熊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伐庭,你說我怎么就攤上這事粉渠。” “怎么了圾另?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵霸株,是天一觀的道長。 經(jīng)常有香客問我集乔,道長去件,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任扰路,我火速辦了婚禮尤溜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汗唱。我一直安慰自己宫莱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布哩罪。 她就那樣靜靜地躺著授霸,像睡著了一般巡验。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碘耳,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天显设,我揣著相機(jī)與錄音,去河邊找鬼辛辨。 笑死捕捂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斗搞。 我是一名探鬼主播绞蹦,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼榜旦!你這毒婦竟也來了幽七?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤溅呢,失蹤者是張志新(化名)和其女友劉穎澡屡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咐旧,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驶鹉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铣墨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片室埋。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖伊约,靈堂內(nèi)的尸體忽然破棺而出姚淆,到底是詐尸還是另有隱情,我是刑警寧澤屡律,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布腌逢,位于F島的核電站,受9級(jí)特大地震影響超埋,放射性物質(zhì)發(fā)生泄漏搏讶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一霍殴、第九天 我趴在偏房一處隱蔽的房頂上張望媒惕。 院中可真熱鬧,春花似錦来庭、人聲如沸妒蔚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽面睛。三九已至絮蒿,卻和暖如春尊搬,著一層夾襖步出監(jiān)牢的瞬間叁鉴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工佛寿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留幌墓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓冀泻,卻偏偏與公主長得像常侣,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子弹渔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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