最近計劃重新鞏固一下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ā)
- 編譯階段:
以上全都為不帶參數的方法編譯后的C函數結構:objc_msgSend(receiver谋旦,selector)
帶參數的方法被編譯成C函數的結構:objc_msgSend(receiver剩失,selector,org1册着,org2拴孤,…)
- 運行階段:
在 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
圖中 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í)行了方法
_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í)行了兩次。
2. 消息轉發(fā)
當動態(tài)解析失敗折晦,并沒有獲取到有效的 IMP 時钥星,系統(tǒng)會做第二次補救措施——消息轉發(fā)。
消息轉發(fā)提供了三個方法函數:
- (id)forwardingTargetForSelector:(SEL)aSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-
- (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 博客