runtime的那些事(一)——runtime基礎介紹

最近計劃重新鞏固一下iOS開發(fā)的底層知識绩社。面對當下環(huán)境,作為一名合格的開發(fā)者,只注重工具的使用是行不通的清女,修煉好底層系統(tǒng)知識的內功才是硬道理

該文章目錄:

一、 什么是runtime晰筛?
二嫡丙、 runtime 版本
三、 與 runtime 的三種交互方式
四传惠、 消息機制的基本原理與執(zhí)行流程
五迄沫、 動態(tài)解析與消息轉發(fā)


一稻扬、什么是 runtime卦方?

都說 Objective-C 是一門動態(tài)語言。首先泰佳,動態(tài)與靜態(tài)語言最大的區(qū)別盼砍,就是動態(tài)語言將數據類型的檢查等決策盡可能地從程序編譯時推遲到了運行時。只要有可能逝她,runtime 就會動態(tài)地完成任務浇坐。這意味著 Objective-C 語言不僅需要編譯器,還需要 runtime 來執(zhí)行編譯代碼黔宛。
runtime 是一套用C語言提供的 API近刘,Objective-C 代碼最終都會被編譯器轉化為運行時代碼,通過消息機制決定了不同函數調用或轉發(fā)方式臀晃,因此 runtime成為了 Objective-C 作為動態(tài)語言使用的基礎觉渴。


二、runtime 版本

runtime 目前共有兩個版本徽惋, Legacy 與 Modern 版本案淋,與之相對應的編程接口分別是 Objective-C 1.0 與 2.0。Legacy 版本主要用于32位的Mac OS X平臺上開發(fā)险绘,而 Modern 版本用于 iPhone 程序與 Mac OS X 10.5以及更新版本系統(tǒng)中的64位程序踢京。
兩個版本最典型的區(qū)別誉碴,就是 Modern 版本中若更改類中實例變量的布局,則不必重新編譯繼承自該類的類瓣距。對于 iOS 開發(fā)者來說黔帕,我們只需要關注 Modern 版本即現(xiàn)行版本的runtime 即可。


三蹈丸、與 runtime 交互方式

接下來會梳理當 NSObject 類與 runtime 交互時蹬屹,runtime 是如何動態(tài)加載新類以及將消息轉發(fā)給其它對象的。

1. Objective-C 源代碼

平時開發(fā)中編寫的 Objective-C 代碼白华,其背后是 runtime 的運行工作慨默。類、方法弧腥、協(xié)議等都由 runtime 轉化成C語言后用數據結構來定義厦取。

2. Foundation 框架下 NSObject 類的方法

在 iOS 類體系中,絕大部分Objective-C 類繼承根類是 NSObject 類(NSProxy類除外管搪,NSProxy定位更適合作為消息轉發(fā)的代理抽象類)虾攻,其本身就提供了一些具有動態(tài)特性的api。

- (NSString *)description  //返回當前類的描述信息

+ (Class)class  //方法返回對象的類更鲁;

- (BOOL)isKindOfClass:(Class)aClass  //判斷對象是否屬于指定類以及其派生類

- (BOOL)isMemberOfClass:(Class)aClass  //檢查對象是否屬于指定類

- (BOOL)respondsToSelector:(SEL)aSelector    //檢查對象是否響應指定的消息霎箍;

+ (BOOL)conformsToProtocol:(Protocol *)protocol   //檢查對象是否實現(xiàn)了指定協(xié)議類;

- (IMP)methodForSelector:(SEL)aSelector    //返回指定方法實現(xiàn)IMP的地址澡为。

3. runtime 系統(tǒng)提供的函數

若要直接使用 runtime 提供的函數漂坏,必須先引入#import <objc/runtime.h>
通過一個最簡單的例子來看下 Objective-C 代碼是如何轉化成 runtime 的C函數。

Class testClass = [TestClass class];
//等價于:Class testClass = objc_getClass("TestClass");
    
TestClass *test = [TestClass alloc];
//等價于:TestClass *test = ((id (*)(id, SEL))(void *)objc_msgSend)((id)testClass, sel_registerName("alloc"));
//簡化后:TestClass *test = objc_msgSend(testClass, sel_registerName("alloc"))

TestClass *testInstance = [test init];
//等價于:TestClass *testInstance = ((id (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("init"));
//簡化后:TestClass *testInstance = objc_msgSend(test, sel_registerName("init")

[testInstance testMethod];
//等價于:objc_msgSend(testInstance, @selector(testMethod));
//等價于:objc_msgSend(testInstance, sel_registerName("testMethod"));

四媒至、 消息機制的基本原理與執(zhí)行流程

在上述最簡單的Objective-C代碼通過 runtime 的C函數轉化后顶别,可以發(fā)現(xiàn):

  • 所有的 Objective-C 方法調用都會在編譯時轉化成C函數 objc_msgSend 的調用
  • objc_msgSend 方法一定會有兩個參數:消息接收者消息方法名稱

runtime 的核心是消息機制拒啰,其執(zhí)行過程大致可分為三個部分:消息發(fā)送驯绎、動態(tài)方法解析消息轉發(fā)

  1. 編譯階段:
    以上全都為不帶參數的方法編譯后的C函數結構:objc_msgSend(receiver谋旦,selector)
    帶參數的方法被編譯成C函數的結構:objc_msgSend(receiver剩失,selector,org1册着,org2拴孤,…)
  2. 運行階段:
    在 recevier(消息接收者)尋找對應的 selector(消息方法名稱)時,
    1. 首先會檢測 selector 是否要忽略
    2. 其次指蚜,檢查 receiver 是否為 nil 對象乞巧,Objective-C 中是允許一個 nil 對象執(zhí)行任何一個方法而不會 Crash,究其原因在于會被直接 return 忽略掉
    3. 當以上兩步沒問題后摊鸡,將開始查找該類的 IMP绽媒,默認先從 cache 中尋找蚕冬,若命中則執(zhí)行對應的方法
    4. 若 cache 中無法命中,則會嘗試從方法列表 methodLists 中尋找
    5. 若方法列表也未找到是辕,則會到向上查找囤热,從父類的方法列表里尋找,一直找到 NSObject 類為止获三,正如下圖中類關系

此處關于消息發(fā)送流程旁蔼,引用一張已被用爛的類關系圖:


6. 若 recevier 最終無法找到對應的 selector ,則執(zhí)行消息動態(tài)解析疙教,由負責動態(tài)的添加方法實現(xiàn)
7. 若 receiver 沒有實現(xiàn)消息動態(tài)解析棺聊,則會執(zhí)行消息重定向,將消息轉發(fā)給可以處理消息的接收者
8. 若消息轉發(fā)也沒有實現(xiàn)贞谓,則會報錯消息無法識別限佩、方法找不到錯誤unrecognzied selector sent to instance并程序 Crash


五、動態(tài)解析與消息轉發(fā)

之前讓我能夠快速理解動態(tài)解析與消息轉發(fā)流程裸弦,最常用的祟同,就是對象、類去調用一個未添加 IMP 實現(xiàn)的方法理疙,去查看消息機制轉發(fā)執(zhí)行的過程晕城。
借助 runtime 提供的一個消息打印函數extern void instrumentObjcMessageSends(BOOL);
其打印結果會輸出到 /private/tmp/msgSend-XXX

runtime 消息打印

圖中 testClass 類繼承自 NSObject 類,其中 walks 方法只在頭文件中進行了聲明窖贤,但未實現(xiàn) IMP砖顷。

此處需留意一個知識點:

對象方法:存在于與類的實例方法列表
類方法:存在于元類的實例方法列表中,即類方法是以實例方法的形式存放在元類中
一圖勝千言

1. 動態(tài)解析

當一個對象或類嘗試去執(zhí)行一個未實現(xiàn) IMP 的方法主之,消息最終無法正常執(zhí)行時择吊,會觸發(fā) + (BOOL)resolveInstanceMethod:(SEL)sel+ (BOOL)resolveClassMethod:(SEL)sel
這是系統(tǒng)為我們提供的第一次解決 IMP 未命中機會,可以為對象動態(tài)添加 IMP 方法解析槽奕。
最終通過runtime中的class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)函數實現(xiàn)。

/**
 運行時方法:向指定類中添加特定方法實現(xiàn)的操作
 @param cls 被添加方法的類
 @param name selector方法名
 @param imp 指向實現(xiàn)方法的函數指針
 @param types imp函數實現(xiàn)的返回值與參數類型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

以下分別是對象實例房轿、類動態(tài)解析方法的用法

//對象動態(tài)解析方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
  NSLog(@"執(zhí)行了實例動態(tài)方法解析:%s", __func__);
  if (sel == @selector(walks)) {
    Method runMethod = class_getInstanceMethod(self, @selector(runInstance));
    IMP runIMP = method_getImplementation(runMethod);
    const char* types = method_getTypeEncoding(runMethod);
    NSLog(@"%s", types);
    return class_addMethod(self, sel, runIMP, types);
  }
  return [super resolveInstanceMethod:sel];
}
//類動態(tài)解析方法
+ (BOOL)resolveClassMethod:(SEL)sel {
  NSLog(@"執(zhí)行了類動態(tài)方法解析:%@----->%s", NSStringFromClass(self), __func__);
  if (sel == @selector(walk)) {
        Class originClass = objc_getMetaClass([NSStringFromClass(self) UTF8String]);
        Method runMethod = class_getInstanceMethod(originClass, @selector(run));
        IMP runIMP = method_getImplementation(runMethod);
        const char* types = method_getTypeEncoding(runMethod);
        NSLog(@"%s", types);
        return class_addMethod(originClass, sel, runIMP, types);
  }
  return [super resolveClassMethod:sel];
}

上述實現(xiàn)動態(tài)解析中粤攒,若要使其成功執(zhí)行就必須存在已經實現(xiàn)了的方法,比如上面用到的對象方法- (void)runInstance類方法+ (void)run囱持。
關于types參數夯接,即 IMP 函數實現(xiàn)的返回值與參數類型,可以參考官方說明文檔Objective-C Runtime Programming Guide
在動態(tài)解析方法過程中
對象方法 執(zhí)行的順序為


類方法 執(zhí)行的順序為

關于消息轉發(fā)暫且放在一邊纷妆,在類方法動態(tài)解析過程中盔几,發(fā)現(xiàn)執(zhí)行了兩次+ (BOOL)resolveClassMethod:(SEL)sel解析;而在對象方法動態(tài)解析過程中掩幢,+ (BOOL)resolveInstanceMethod:(SEL)sel方法卻只執(zhí)行了一次逊拍。
通過 LLDB 的bt分解每一步上鞠,在+ (BOOL)resolveClassMethod:(SEL)sel中添加斷點。
兩次執(zhí)行類方法動態(tài)解析分析

第一次芯丧,上面紅色邊框中芍阎,先執(zhí)行了方法_objc_msgSend_uncached,然后走方法lookUpImpOrForward缨恒,再執(zhí)行到方法_class_resolveMethod谴咸,這個流程其實是尋找 IMP 的過程;若沒有找到骗露,就會進入動態(tài)解析流程岭佳;
第二次,下面紅色邊框中的信息萧锉,發(fā)現(xiàn)了消息轉發(fā)相關方法的執(zhí)行動作驼唱,也就是說第二次時從消息轉發(fā)過來的,意味著第一次動態(tài)解析失敗了驹暑。在消息轉發(fā)過來之后玫恳,接著會去執(zhí)行class_getInstanceMethod方法,而這個方法卻是實例方法動態(tài)解析所用到的优俘。而關于類方法的存放位置京办,首先它是類的類方法,其次也是元類的實例方法帆焕,按照消息執(zhí)行向上傳遞的規(guī)則惭婿,在尋找類方法 IMP 過程中多執(zhí)行了一次,也就是我們看到的兩次類方法動態(tài)解析執(zhí)行叶雹。
通過下面這張圖可以更好地理解 isa指針在類中向上傳遞查找順序财饥,也正好佐證了上述類方法在動態(tài)解析過程為什么執(zhí)行了兩次。
isa指針查找順序圖

2. 消息轉發(fā)

當動態(tài)解析失敗折晦,并沒有獲取到有效的 IMP 時钥星,系統(tǒng)會做第二次補救措施——消息轉發(fā)。
消息轉發(fā)提供了三個方法函數:

  1. - (id)forwardingTargetForSelector:(SEL)aSelector
  2. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
  3. - (void)forwardInvocation:(NSInvocation *)anInvocation
    當動態(tài)解析失敗后满着,進入消息轉發(fā)流程
    首先谦炒,會執(zhí)行函數- (id)forwardingTargetForSelector:(SEL)aSelector。該函數目的在于风喇,通過該函數系統(tǒng)會將 SEL 嘗試轉發(fā)給其它對象宁改,而且此對象不能是 self 與 nil
- (id)forwardingTargetForSelector:(SEL)aSelector {
  //若沒有添加新函數時魂莫,系統(tǒng)會提供機會將該 SEL 轉發(fā)給其它對象还蹲。
  NSLog(@"消息嘗試轉發(fā)給其它對象:%s", __func__);
  if (aSelector == @selector(walk)) {
    return [testTwo new];
  }
  return [super forwardingTargetForSelector:aSelector];
}

但該函數返回了 nil 或者 self 時,此時系統(tǒng)會提供最后一次尋找 IMP 的機會。接下來會執(zhí)行- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector函數谜喊,去詢問該消息是否有效潭兽,并嘗試讓其生成一個函數的簽名,若簽名無效返回 nil 并拋出異常锅论;若不是 nil 讼溺,再由函數符號執(zhí)行器- (void)forwardInvocation:(NSInvocation *)anInvocation去執(zhí)行。

// 函數簽名生成最易,告訴系統(tǒng)該消息是有效的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  NSLog(@"消息重定向:%s", __func__);
  NSString *selString = NSStringFromSelector(aSelector);
  if ([selString isEqualToString:@"walks"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  }
  return [super methodSignatureForSelector:aSelector];
}

當函數生成簽名后怒坯,系統(tǒng)會嘗試執(zhí)行方法- (void)forwardInvocation:(NSInvocation *)anInvocation

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  NSLog(@"消息重定向執(zhí)行函數:%s", __func__);
  testTwo *twoObj = [testTwo new];
  SEL selector = [anInvocation selector];
  if ([twoObj respondsToSelector:selector]) {
    [anInvocation invokeWithTarget:twoObj];
  } else {
    return [super forwardInvocation:anInvocation];\
  }
}

上述代碼中,嘗試將此類中的消息轉發(fā)給了由 testTwo 類創(chuàng)建的對象實例——twoObj 并去執(zhí)行藻懒。

消息轉發(fā)流程是把未識別的消息分發(fā)給了其他不同接收對象剔猿,又或者是將所有未識別消息發(fā)送給同一個接收對象,其具體實現(xiàn)方式完全可以自由控制嬉荆。而這一切的前提归敬,是消息接收對象不能有指定方法的實現(xiàn),才能有機會去執(zhí)行消息轉發(fā)鄙早。

函數簽名

關于消息轉發(fā)中汪茧,出現(xiàn)了一個名詞:函數簽名,其對應類為 NSMethodSignature
問題來了限番,何為函數簽名舱污?

函數簽名中具體實現(xiàn)的方案被稱為 類型編碼(Type Encoding) 。是為了協(xié)助 runtime 系統(tǒng)弥虐,編譯器將存儲記錄每個函數方法的返回值類型扩灯、參數類型編碼信息,以字符串形式與對應方法 selector 進行關聯(lián)的一種編碼方案霜瘪。NSMethodSignature 對象正是用于管理類型編碼的存在珠插。
當我們想要獲取某個方法的類型編碼,可以使用 @encode 編譯器指令來獲取某個指定類型的字符串編碼颖对。這些類型既可以是基本類型捻撑,也可以是結構體、類等類型惜互,任何可作為 sizeof() 操作參數的類型都可用于 @encode 布讹。
Objective-C 中所有類型編碼參照Type Encoding

但是,Objective-C 不支持long double 類型训堆。@encode(long double) 返回d,和double編碼一樣

NSObject 類聲明一個實例變量白嘁、isa坑鱼,是作為類型類。
通過 @encode 指令獲取時并不會返回,但在協(xié)議中去聲明方法時鲁沥, runtime 會使用下面額外的編碼列表來限定類型呼股。


NSInvocation

NSInvocation類的對象是調用函數的另一種表現(xiàn)形式,將對象画恰、方法選擇器彭谁、參數以及返回值等各種信息,都封裝到此類的對象中允扇,再通過 invoke 函數去執(zhí)行被調用函數缠局,其思想本質是 命令者模式 的展現(xiàn)。


消息轉發(fā)小擴展——實現(xiàn)Objective-C 多繼承

利用消息轉發(fā)可以實現(xiàn) Objective-C 語言編程的多繼承效果考润。兩個沒有繼承關系的類狭园,當一個類執(zhí)行了未能實現(xiàn)的方法時,可以將該方法轉發(fā)給另一個可執(zhí)行該方法的類去執(zhí)行糊治,這樣就可以靈活的彌補 Objective-C 本身不支持多繼承的特性唱矛,也避免因為層層繼承導致類文件結構臃腫、邏輯復雜井辜。


以上就是對 Objective-C 語言的幕后工作者——runtime的基礎介紹绎谦。篇幅有限,許多 runtime 中用到的數據結構與類定義直接略過粥脚,后面會專門用一篇文章詳細說明窃肠,這樣更有助于理解 runtime 的底層邏輯實現(xiàn)。


該文章首次發(fā)表在 簡書:我只不過是出來寫寫代碼 博客阿逃,并自動同步至 騰訊云:我只不過是出來寫寫iOS 博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末铭拧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恃锉,更是在濱河造成了極大的恐慌搀菩,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件破托,死亡現(xiàn)場離奇詭異肪跋,居然都是意外死亡,警方通過查閱死者的電腦和手機土砂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門州既,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人萝映,你說我怎么就攤上這事吴叶。” “怎么了序臂?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵蚌卤,是天一觀的道長实束。 經常有香客問我,道長逊彭,這世上最難降的妖魔是什么咸灿? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮侮叮,結果婚禮上避矢,老公的妹妹穿的比我還像新娘。我一直安慰自己囊榜,他們只是感情好审胸,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锦聊,像睡著了一般歹嘹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上孔庭,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天尺上,我揣著相機與錄音,去河邊找鬼圆到。 笑死怎抛,一個胖子當著我的面吹牛,可吹牛的內容都是我干的芽淡。 我是一名探鬼主播马绝,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挣菲!你這毒婦竟也來了富稻?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤白胀,失蹤者是張志新(化名)和其女友劉穎椭赋,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體或杠,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡哪怔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了向抢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片认境。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖挟鸠,靈堂內的尸體忽然破棺而出叉信,到底是詐尸還是另有隱情,我是刑警寧澤艘希,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布茉盏,位于F島的核電站鉴未,受9級特大地震影響枢冤,放射性物質發(fā)生泄漏鸠姨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一淹真、第九天 我趴在偏房一處隱蔽的房頂上張望讶迁。 院中可真熱鬧,春花似錦核蘸、人聲如沸巍糯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祟峦。三九已至,卻和暖如春徙鱼,著一層夾襖步出監(jiān)牢的瞬間宅楞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工袱吆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留厌衙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓绞绒,卻偏偏與公主長得像婶希,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蓬衡,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容

  • 我們常常會聽說 Objective-C 是一門動態(tài)語言喻杈,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,172評論 0 7
  • 繼上Runtime梳理(四) 通過前面的學習狰晚,我們了解到Objective-C的動態(tài)特性:Objective-C不...
    小名一峰閱讀 741評論 0 3
  • 本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識筒饰,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 792評論 0 4
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,129評論 0 9
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 727評論 0 2