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