什么是Runtime力细?
概念
Objective-C是基于C語言加入面向?qū)ο筇匦院拖⑥D(zhuǎn)發(fā)機制的動態(tài)語言,這就是說它不僅需要一個編譯器诅蝶,還需要Runtime系統(tǒng)動態(tài)的創(chuàng)建類和對象候生,進行消息發(fā)送和轉(zhuǎn)發(fā)。關(guān)于Runtime概念眾說紛紜湾趾。理解Runtime芭商,我們從源碼開始.... 源碼介紹 Runtime在實際開發(fā)中,其實就是一組C語言函數(shù)搀缠。
官方介紹:官方文檔
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.
怎么理解這句話呢铛楣?盡量將決定放到運行的時候,而不是在編譯和鏈接過程...如圖所示
Clang 是什么鬼艺普?
Clang是一個C語言簸州、C++、Objective-C歧譬、Objective-C++語言的輕量級編譯器岸浑。
官方介紹:官方文檔
clang is a C, C++, and Objective-C compiler which encompasses preprocessing, parsing, optimization, code generation, assembly, and linking.
源代碼:main.m
//
// main.m
// YJDoctor
//
// Created by YJHou on 2015/10/25.
// Copyright ? 2015年 houmanager@hotmail.com. All rights reserved.
//
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSObject *obj = [[NSObject alloc] init];
NSLog(@"-->%@", obj);
}
return 0;
}
編譯器:
clang -rewrite-objc main.m
生成了mian.cpp文件,打開查看源碼:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_dr_k2drvnh548q5293brg9wmgzc0000gn_T_main_2a142f_mi_0, obj);
}
return 0;
}
似乎看到了Runtime的影子:objc_msgSend瑰步、objc_getClass矢洲、sel_registerName...
尋找Runtime的來源
打開資源地址:/usr/include/objc 會發(fā)現(xiàn)如下文件:
為什么要熟悉掌握Runtime機制?
Runtime 在實際開發(fā)中缩焦,會經(jīng)常用到嗎桑李?這個答案是肯定的盅蝗。但是Runtime用的好不好在于理解程度,理解的好代碼質(zhì)量高效實用;用的不好勃痴,容易自己造坑。在實際開發(fā)中排宰,我并不是推薦大家熟悉靈活的運用底層的東西媚值,而是熟悉知道底層的運行機制。要不已經(jīng)封裝好看又好用的API干啥使。
Runtime 具體都干啥使用靴拱?
比如:動態(tài)添加屬性垃喊、動態(tài)添加方法、方法交換袜炕、字典模型轉(zhuǎn)換
面試經(jīng)歷: 曾經(jīng)一次面試本谜,面試官說類別能不能設(shè)置屬性?咋一聽偎窘,條件反射類別還能設(shè)置屬性乌助,什么鬼,后來一想面試官問的是怎么給類別添加屬性吧陌知,用詞準確很重要他托,添加和設(shè)置概念是不同的。面試官馬上更正是添加不是設(shè)置仆葡。
深刻理解Runtime的底層原理是什么樣子的赏参?
首先了解Runtime的數(shù)據(jù)結(jié)構(gòu)
打開runtime.h會看到數(shù)據(jù)結(jié)構(gòu)如圖所示:
- id : typedef struct objc_object *id;
- SEL : typedef struct objc_selector *SEL;
- Class : Class 也有一個 isa 指針,指向其所屬的元類(meta)沿盅。
- super_class:指向其超類把篓。
- name:是類名。
- version:是類的版本信息腰涧。
- info:是類的詳情韧掩。
- instance_size:是該類的實例對象的大小。
- ivars:指向該類的成員變量列表窖铡。
- methodLists:指向該類的實例方法列表疗锐,它將方法選擇器和方法實現(xiàn)地址聯(lián)系起來。methodLists 是指向 ·objc_method_list 指針的指針费彼,也就是說可以動態(tài)修改
- methodLists 的值來添加成員方法滑臊,這也是 Category 實現(xiàn)的原理,同樣解釋了 Category 不能添加屬性的原因敌买。
- cache:Runtime 系統(tǒng)會把被調(diào)用的方法存到 cache 中(理論上講一個方法如果被調(diào)用简珠,那么它有可能今后還會被調(diào)用),下次查找的時候效率更高虹钮。
- protocols:指向該類的協(xié)議列表(對象方法列表的擴展)聋庵。
理解底層原理要從這三張圖說起:
Messaging 官方介紹
- 剛開始clang mian.m文件可以看出,Runtime System 會將方法調(diào)用轉(zhuǎn)化為消息發(fā)送(objc_msgSend), 并把方法的調(diào)用者和方法選擇器當做參數(shù)傳遞芙粱。
- 此時祭玉,方法調(diào)用者會通過isa指針來找到其所屬的類,然后在cache或者methodLists中查找該方法春畔,如果能找到就跳到對應的方法(IMP)中執(zhí)行脱货。
- 如果在類中沒有找到該方法岛都,會檢查本類是否有動態(tài)加載的方法來處理該消息,如果還是沒有振峻,通過super_class網(wǎng)上一級父類查找, 如果一直到NSObject都沒找到該方法的話臼疫,這種情況,就該消息轉(zhuǎn)發(fā)上場了扣孟。
- 從數(shù)據(jù)結(jié)構(gòu)中看到烫堤,methodLists 指向該類的實例方法列表,那么類方法在哪里凤价?類方法存儲在元類中鸽斟,Class通過isa指針即可找到所屬的元類。
Dynamic Method Resolution 官方介紹
從有圖可以看出利诺,ObjC 類本身同時也是一個對象富蓄,為了處理類和對象的關(guān)系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西慢逾,類對象所屬類型就叫做元類立倍,它用來表述類對象本身所具備的元數(shù)據(jù)。類方法就定義于此處氛改,因為這些方法可以理解成類對象的實例方法帐萎。
每個類僅有一個類對象比伏,而每個類對象僅有一個與之相關(guān)的元類胜卤。當你發(fā)出一個類似 [NSObject alloc] 的消息時,你事實上是把這個消息發(fā)給了一個類對象 (Class Object) 赁项,這個類對象必須是一個元類的實例葛躏,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類悠菜。所有的元類的方法列表都有能夠響應消息的類方法舰攒。所以當 [NSObject alloc] 這條消息發(fā)給類對象的時候,objc_msgSend() 會去它的元類里面去查找能夠響應消息的方法悔醋,如果找到了摩窃,然后對這個類對象執(zhí)行方法調(diào)用。
在Runtime System沒有在本類的method_lists沒有找到匹配的實現(xiàn)方法時芬骄,我們可以動態(tài)的添加一個方法猾愿,這是開始進行消息轉(zhuǎn)發(fā)(messaging forward)前的第一階段,例如我們用@dynamic關(guān)鍵字在類的實現(xiàn)文件中修飾一個屬性:這表明我們會為這個屬性動態(tài)提供存取方法账阻,編譯器不會默認為我們生成setPropertyName:和propertyName方法蒂秘,而需要我們動態(tài)提供。
同樣我們可以通過分別重載resolveInstanceMethod:和resolveClassMethod:方法分別添加實例方法實現(xiàn)和類方法實現(xiàn)淘太。因為當 Runtime 系統(tǒng)在Cache和方法分發(fā)表中來給程序員一次動態(tài)添加方法實現(xiàn)的機會姻僧。我們需要用class_addMethod函數(shù)完成向特定類添加特定方法實現(xiàn)的操作:
Message Forwarding 官方介紹
消息轉(zhuǎn)發(fā)分為兩大階段规丽。
- 第一階段先征詢接收者,所屬的類撇贺,看其是否能動態(tài)添加方法赌莺,以處理當前這個“未知的選擇子”,這叫做“動態(tài)方法解析”松嘶。
- 第二階段涉及“完整的消息轉(zhuǎn)發(fā)機制(full forwarding mechanism)”如果運行期系統(tǒng)已經(jīng)執(zhí)行完第一階段雄嚣,此時,運行期系統(tǒng)會請求我接收者以其它手段來處理消息喘蟆』荷可以細分3小步。
1.首先查找有沒有replacement receiver進行處理蕴轨。若無;
2.運行期系統(tǒng)把Selector相關(guān)信息封裝到NSInvocation對象中;
3.再給一次機會港谊,若依舊未處理則讓NSObject調(diào)用doNotReconizeSelector:
具體看代碼所示:
由此我們可以看到,object_getClass返回的其實是class的metaClass橙弱,即Class這個類對象的類歧寺,這個概念有點繞。梳理一下:Person這么一個類(0x1000f53c8)棘脐,它的isa指針指向其元類(地址0x1000f53f0)斜筐,這個元類的isa指針指向基類NSObject的元類,即根元類(0x1a7919ec8)蛀缝,再遞進一層可以發(fā)現(xiàn)顷链,根元類的isa指針指向自己,這樣就形成了一個完整的閉環(huán)屈梁。
另外objc_getClass 是什么鬼嗤练?
由此可知objc_getClass方法只是單純地返回了Class,而非isa指針指向的Class在讶。
Runtime的應用場景有什么煞抬?
- 實現(xiàn)第一個場景:跟蹤程序每個ViewController展示給用戶的次數(shù),可以通過Method Swizzling替換ViewDidAppear初始方法构哺。創(chuàng)建一個UIViewController的分類革答,重寫自定義的ViewDidAppear方法,并在其+load方法中實現(xiàn)ViewDidAppear方法的交換
- 開發(fā)中常需要在不改變某個類的前提下為其添加一個新的屬性曙强,尤其是為系統(tǒng)的類添加新的屬性残拐,這個時候就可以利用Runtime的關(guān)聯(lián)對象(Associated Objects)來為分類添加新的屬性了
- 三實現(xiàn)字典的模型和自動轉(zhuǎn)換,優(yōu)秀的JSON轉(zhuǎn)模型第三方庫JSONModel旗扑、YYModel等都利用runtime對屬性進行獲取蹦骑,賦值等操作,要比KVC進行模型轉(zhuǎn)換更加強大臀防,更有效率眠菇。閱讀YYModel的源碼可以看出边败,YY大神對NSObject的內(nèi)容進行了又一次封裝,添加了許多描述內(nèi)容捎废。其中YYClassInfo是對Class進行了再次封裝笑窜,而YYClassIvarInfo、YYClassMethodInfo登疗、YYClPropertyInfo分別是對Class的Ivar排截、Method和property進行了封裝和描述。在提取Class的相關(guān)信息時都運用了Runtime辐益。
- JSPatch替換已有的OC方法實行断傲,具體內(nèi)容請參看相關(guān)文檔。