拓展了 C 語言的 Objc 是一種動態的面向對象的語言,Objc 之所以能做到“動態”,就得宜于 Objc Runtime。Objc Runtime實際上是一個用 C 和匯編寫的Runtime庫,職責是運行時加載類的信息,進行消息的分發和轉發。
對比 C 和 Objc:
- C 是靜態的,函數調用在編譯期決定
- OC 是動態的,函數調用實際是在發送消息,Runtime 負責在運行時期找到調用方法。
Objc 對象與類的定義
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
可見每個 Objc 對象都有一個 id 指針指向一個 objc_object 結構體, objc_object 結構體中又有一個指向對象Class的指針 isa,它實際上指向了一個 objc_class 結構體實例,即對象對應的類對象。
Objc 類對象
Objc Runtime中,無論是類的實例對象還是類本身,實際上都是以對象的形式存在。先來看一下類的數據結構:
/*Objc 類的數據結構*/
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
// 指向metaclass
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認為0
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;
isa:可以看出此處的 isa 也是一個指向 objc_class 的指針,證明了類也是對象。普通實例對象的 isa 指針指向的類結構實例稱為class,保存類的普通成員變量和對象方法;class對象的isa指針指向的類結構實例則稱為metaclass,保存類的static成員變量與static類型方法。metaclass 對象的isa指針則指向根 metaclass*(詳見后文)
super_class:指向父類,類如果是頂層根類則為NULL
info: 一些標識信息,如 CLS_CLASS (0x1L) 表示該類為普通 class ;CLS_META (0x2L) 表示該類為 metaclass
cache:緩存最近調用過的方法。對象收到消息的時候,runtime 會根據對象的 isa 指針去查找能響應這個消息的對象。實際上,對象的方法只有一部分是常用的,這種情況下每次都遍歷一遍 methodLists 非常蠢。cache就可以解決這個問題,對象調用過一個方法后,該方法就會被存到cache列表中,下次調用的時候 runtime會優先去 cache中查找
類與對象的繼承層次關系
之前提到普通實例對象的isa指針指向其class對象,class對象的isa指針指向metaclass對象,那么這三者之間的繼承層次關系是怎么呢, 引用一張著名的圖:
可以看出,metaclass 也是對象。從類結構的成員 info 中存有 class 或 metaclass 的標識,也可以看出,class 與 metaclass 共用一套數據結構。
值得注意的是:metaclass 對象的 isa 指針均指向 root metaclass,而 root metaclass的父指針又指向 root class,這就保證了可以通過 metaclass 對象訪問所有方法。
Runtime 如何進行方法調用
在弄清 Objc 類結構以及對象與類,元類(metaclass)之間的關系之后,我們就可以來看看 runtime 是如何進行方法的調用的了。先來看一段函數調用的模擬編譯代碼:
id __strong obj = [[NSObject alloc] init];
/*對應編譯模擬代碼*/
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
可見Objc中的函數調用其實是通過objc_msgSend
這個函數完成的。
objc_msgSend 方法
objc_msgSend 方法的調用流程如下:
檢查 selector 是否要忽略:如在ARC下,retain,release方法要被忽略。
target 是否為 nil:這就是為什么 Objc 中 nil 對象可以調用方法但不會崩潰的原因,因為在這一步就被忽略了。
查找方法IMP:通過方法的 SEL(可理解為方法的 ID)查找 IMP(函數指針,指向方法實現的首地址),SEL 與 IMP 在方法列表中以鍵值對的方式存儲,查找流程為
(1)先根據對象的 Class 指針找到類對象;
(2)在類對象的 cache 和方法列表查找;
(3)在 superclass 中重復(2)(3)。
在(1)(2)(3)中一但找到實現,就跳到實現地址執行。消息轉發:當經過1,2,3還沒找到IMP就要進行消息轉發了,如果轉發之后還無法找到能調用的方法程序將崩潰。
注:方法查找與消息轉發的內容將在下幾篇博客詳細介紹。