Runtime簡介
Rutime又叫運行時, 是一套底層的C語言API, 是iOS系統(tǒng)的核心之一. 開發(fā)者在編碼過程中, 可以給任意一個對象發(fā)送消息, 在編譯階段只是確定了要向接受者發(fā)送這條消息, 而接受者將要如何響應(yīng)和處理這條消息, 那就要看運行時來決定.
C語言中, 在編譯期, 函數(shù)的調(diào)用就會決定調(diào)用哪個函數(shù). 而OC的函數(shù), 屬于動態(tài)調(diào)用過程, 在編譯期并不能決定真正調(diào)用哪個函數(shù), 只有在真正運行時才會根據(jù)函數(shù)的名稱找到對應(yīng)的函數(shù)來調(diào)用.
Objective-C 是一門動態(tài)語言, 這意味著它不僅需要一個編譯器, 也需要一個運行時系統(tǒng)來動態(tài)的創(chuàng)建類和對象, 進行消息傳遞和轉(zhuǎn)發(fā).
NSObject的定義如下
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在Objc2.0之前
objc_class
源碼如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在這里可以看到, 一個類中, 有超類的指針, 類名, 版本的信息. ivars是objc_ivar_list成員變量列表的指針; methodLists是指向objc_method_list指針的指針. *methodLists是指向方法列表的指針. 動態(tài)修改 * methodLists的值就可以添加成員方法, 這也是Category實現(xiàn)的原理, 同樣解釋了Category不能添加成員變量的原因.
tip: 關(guān)于Category
我們知道, 所有的OC類和對象, 在runtime層都是用struct表示的, Category也不例外, 在runtime層, Category用結(jié)構(gòu)體category_t定義, 它包含了:
- 類的名字
- 類
- category中所有給類添加的實例方法的列表
- category中所有添加的類方法的列表
- category實現(xiàn)的所有協(xié)議的列表
- category中添加的所有屬性
typedef struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
} category_t;
從category的定義也可以看出category的可為(可以添加實例方法, 類方法, 甚至可以可以實現(xiàn)協(xié)議, 添加屬性(不含成員變量和getter,setter方法))和不可謂(無法添加實例變量).
在Objc2.0之后
objc_class的定義就變成這樣了:
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
從上述源碼中, 我們可以看到, Objective-C對象都是C語言結(jié)構(gòu)體實現(xiàn)的, 類也是一個對象, 叫類對象. 在objc2.0中, 所有的對象都會包含一個isa_t類型的結(jié)構(gòu)體成員變量isa, 這也是所有對象的第一個成員變量.
當(dāng)一個對象的實例方法被調(diào)用的時候, 會通過isa找到相應(yīng)的類, 然后在該類的clas_data_bits_t中去查找方法. class_data_bits_t是指向了類對象的數(shù)據(jù)區(qū)域, 在該數(shù)據(jù)區(qū)域內(nèi)查找相應(yīng)方法的對應(yīng)實現(xiàn),即IMP.
但是在我們調(diào)用類方法的時候, 類對象的isa又指向的是哪里呢? 這里為了和對象查找方法的機制一致, 遂引入了元類(meta-class)的概念.
實例對象的調(diào)用實例方法時, 通過對象的isa在類中獲取方法的實現(xiàn), 類對象的類方法調(diào)用時, 通過類的isa在元類中獲取方法的實現(xiàn).
meta-class之所以重要, 是因為它存儲著一個類的所有類方法, 每個類都會有單獨的meta-class, 因為每個類的類方法基本不可能完全相同.
下圖很好的描述了對象, 類, 元類之間的關(guān)系:
[圖片上傳失敗...(image-2eea5d-1515141587282)]
我們其實應(yīng)該明白, 類對象和元類對象都是唯一的, 對象是可以在運行時創(chuàng)建無數(shù)個的. 而在main方法執(zhí)行之前, 從dyld(動態(tài)鏈接器)到runtime這期間, 類對象和元類對象在這期間被創(chuàng)建.
tip: iOS程序main函數(shù)之前發(fā)生了什么
一個iOS App的main函數(shù)位于main.m中, 是程序的入口.
整個事件由dyld主導(dǎo), 完成運行環(huán)境的初始化后, 配合imageloader將二進制文件加載內(nèi)存, 動態(tài)鏈接依賴庫, 并由runtime負(fù)責(zé)加載成objc定義的結(jié)構(gòu), 所有初始化工作結(jié)束后, dyld調(diào)用真正的main函數(shù).值得說明的是, 這個過程遠(yuǎn)比寫出來的要復(fù)雜, 這里只提到了runtime這個分支, 還有像GCD
,XPC
等重頭的系統(tǒng)庫初始化分支沒有提及. 總結(jié)起來就是main函數(shù)執(zhí)行之前, 系統(tǒng)做了很多的加載和初始化工作, 但都被很好的隱藏了, 我們無需關(guān)心.
當(dāng)這個一切都結(jié)束時, dyld會清理現(xiàn)場, 將調(diào)用棸⑻樱回歸, 只剩下main函數(shù), 孤獨的main函數(shù), 看上去是程序的開始, 卻是一段精彩的終結(jié).
下面代碼輸出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self和super的區(qū)別: self是類的一個隱藏參數(shù), 每個方法的實現(xiàn)的第一個參數(shù)即為self; super并不是隱藏參數(shù), 它實際上只是一個"編譯器標(biāo)示符", 它負(fù)責(zé)告訴編譯器當(dāng)調(diào)用方法時, 去父類中去查找方法, 而不是從本類中查找方法.
因此在這個問題中, 都是在根類中找方法實現(xiàn), 要明確的是, 發(fā)送消息(調(diào)用方法)的主體是son, 接受消息的主體也是son, 所有打印的都是son.
消息發(fā)送和轉(zhuǎn)發(fā)
objc_msgSend函數(shù)
最初接觸到OC 的 Runtime, 一定是從[receiver message]這里開始的, [receive message] 會被編譯器轉(zhuǎn)化為:
id objc_msgSend ( id self, SEL op, ... );
這是一個可變參數(shù)函數(shù), 第二個桉樹類型是SEL, SEL在OC中是selector方法選擇器
typedef struct objc_selector *SEL;
objc_selector是一個映射到方法的C字符串. 需要注意的是@selector()選擇子只與函數(shù)名有關(guān). 不同類中相同名字的方法所對應(yīng)的方法選擇器是相同的, 即使方法名字相同二變量類型不同也會導(dǎo)致它們具有相同的方法選擇器, 由于這點特性, 也導(dǎo)致了OC不支持函數(shù)重載.(ps: 函數(shù)重載是指方法名相同而參數(shù)不同的函數(shù))
在receiver拿到對應(yīng)的selector之后, 如果自己無法執(zhí)行這個方法, 那么該條消息會被轉(zhuǎn)發(fā), 或者臨時動態(tài)的添加方法實現(xiàn), 如果轉(zhuǎn)發(fā)到最后依舊沒法處理, 程序就會崩潰.
所以編譯器僅僅是確定了要發(fā)送消息, 而消息如何處理是要在運行期解決的事情.
總結(jié)一下objc_msgSend會做的幾件事情:
檢測這個selector是不是要忽略的.
-
檢測target是不是為nil.
如果這里有相應(yīng)的nil的處理函數(shù), 就跳轉(zhuǎn)到相應(yīng)的函數(shù)中, 如果沒有處理nil的函數(shù), 就自動清理現(xiàn)場并返回. 這一點就是為何在OC中給nil發(fā)送消息不會崩潰的原因.
-
確定不是給nil發(fā)消息之后, 在該class的緩存中查找方法對應(yīng)的IMP實現(xiàn).
如果找到, 就跳轉(zhuǎn)進去執(zhí)行. 如果沒有找到, 就在父類方法列表里面繼續(xù)查找, 一直找到NSOject為止.
如果還沒有找到, 那就需要開始消息轉(zhuǎn)發(fā)階段了. 至此, 發(fā)送消息階段完成. 這一階段主要完成的是通過select()快速查找IMP的過程.
消息轉(zhuǎn)發(fā)Message Forwarding階段
到了轉(zhuǎn)發(fā)階段, 會調(diào)用id_objc_msgForward(id self, SEL _cmd,...)方法, 在執(zhí)行_objc_msgForward之后會調(diào)用__objc_forward_handler函數(shù). 當(dāng)我們給一個對象發(fā)送一個沒有實現(xiàn)的方法的時候, 如果其父類也沒有這個方法, 則會崩潰, 報錯信息類似于: unrecognized selector sent to instance呕臂,然后接著會跳出一些堆棧信息. 這些信息就是從這個方法中拋出的.
要設(shè)置轉(zhuǎn)發(fā)只要重寫_objc_forward_handler方法即可, 這一步是替消息找備援接受者, 如果這一步返回的是nil, 那么不就措施就完全的失效了, 接下來未識別的方法崩潰之前, 系統(tǒng)會再做一次完整的消息轉(zhuǎn)發(fā).
Runtime的可以做什么?
- 實現(xiàn)多繼承
- Method Swizzling
- AOP (AOP埋點方案)
- Isa Swizzling
- Associated Object關(guān)聯(lián)對象
- 動態(tài)的增加方法
- NSCoding的自動歸檔和自動解檔
- 字典和模型互相轉(zhuǎn)換