首先,讓我們先對runtime的底層概念梳理下,若想看怎麼用可以翻到底部。
簡介
Runtime 又叫運行時,是一套底層的 C 語言 API,其為 iOS 內部的核心之一,我們平時編寫的 OC 代碼,底層都是基於它來實現的。比如:
[receiver message];// 底層運行時會被編譯器轉化為:objc_msgSend(receiver, selector)// 如果其還有參數比如:[receiver message:(id)arg...];// 底層運行時會被編譯器轉化為:objc_msgSend(receiver, selector, arg1, arg2, ...)
以上你可能看不出它的價值,但是我們需要瞭解的是 Objective-C 是一門動態語言,它會將一些工作放在代碼運行時才處理而並非編譯時間。也就是說,有很多類和成員變數在我們編譯的時是不知道的,而在運行時,我們所編寫的代碼會轉換成完整的確定的代碼運行。
因此,編譯器是不夠的,我們還需要一個運行時系統(Runtime system)來處理編譯後的代碼。
Runtime 基本是用 C 和彙編寫的,由此可見蘋果為了動態系統的高效而做出的努力。蘋果和 GNU 各自維護一個開源的 Runtime 版本,這兩個版本之間都在努力保持一致。
點擊這裡下載蘋果維護的開原始碼。 Runtime 的作用
Objc 在三種層面上與 Runtime 系統進行互動: 通過 Objective-C 原始碼 通過 Foundation 架構的 NSObject 類定義的方法 通過對 Runtime 庫函數的直接調用 Objective-C 原始碼
多數情況我們只需要編寫 OC 代碼即可,Runtime 系統自動在幕後搞定一切,還記得簡介中如果我們調用方法,編譯器會將 OC 代碼轉換成運行時代碼,在運行時確定資料結構和函數。 通過 Foundation 架構的 NSObject 類定義的方法
Cocoa 程式中絕大部分類都是 NSObject 類的子類,所以都繼承了 NSObject 的行為。(NSProxy 類時個例外,它是個抽象超類)
一些情況下,NSObject 類僅僅定義了完成某件事情的模板,並沒有提供所需要的代碼。例如 -description 方法,該方法返回類內容的字串表示,該方法主要用來偵錯工具。NSObject 類並不知道子類的內容,所以它只是返回類的名字和對象的地址,NSObject 的子類可以重新實現。
還有一些 NSObject 的方法可以從 Runtime 系統中擷取資訊,允許對象進行自我檢查。例如: -class方法返回對象的類; -isKindOfClass: 和 -isMemberOfClass: 方法檢查對象是否存在於指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變數); -respondsToSelector: 檢查對象能否響應指定的訊息; -conformsToProtocol:檢查對象是否實現了指定協議類的方法; -methodForSelector: 返回指定方法實現的地址。 通過對 Runtime 庫函數的直接調用
Runtime 系統是具有公用介面的動態共用程式庫。標頭檔存放於/usr/include/objc目錄下,這意味著我們使用時只需要引入objc/Runtime.h標頭檔即可。
許多函數可以讓你使用純 C 代碼來實現 Objc 中同樣的功能。除非是寫一些 Objc 與其他語言的橋接或是底層的 debug 工作,你在寫 Objc 代碼時一般不會用到這些 C 語言函數。對於公用介面都有哪些,後面會講到。我將會參考蘋果官方的 API 文檔。 一些 Runtime 的術語的資料結構
要想全面瞭解 Runtime 機制,我們必須先瞭解 Runtime 的一些術語,他們都對應著資料結構。 SEL
它是selector在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選取器,其實作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector 對方法名進行封裝,以便找到對應的方法實現。它的資料結構是:
typedef struct objc_selector *SEL;
我們可以看出它是個映射到方法的 C 字串,你可以通過 Objc 編譯器器命令@selector() 或者 Runtime 系統的 sel_registerName 函數來擷取一個 SEL 類型的方法選取器。
注意:
不同類中相同名字的方法所對應的 selector 是相同的,由於變數的類型不同,所以不會導致它們調用方法實現混亂。 id
id 是一個參數類型,它是指向某個類的執行個體的指標。定義如下:
typedef struct objc_object *id;struct objc_object { Class isa; };
以上定義,看到 objc_object 結構體包含一個 isa 指標,根據 isa 指標就可以找到對象所屬的類。
注意:
isa 指標在代碼運行時並不總指向執行個體對象所屬的類型,所以不能依靠它來確定類型,要想確定類型還是需要用對象的 -class 方法。
PS:KVO 的實現機理就是將被觀察對象的 isa 指標指向一個中間類而不是真實類型,詳見:KVO章節。 Class
typedef struct objc_class *Class;
Class 其實是指向 objc_class 結構體的指標。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;
從 objc_class 可以看到,一個運行時類中關聯了它的父類指標、類名、成員變數、方法、緩衝以及附屬的協議。
其中 objc_ivar_list 和 objc_method_list 分別是成員變數列表和方法列表:
// 成員變數列表struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE;#ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;} OBJC2_UNAVAILABLE;// 方法列表struct objc_method_list { struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE;#ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE;}
由此可見,我們可以動態修改 *methodList 的值來新增成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。這裡可以參考下美團技術團隊的文章:深入理解 Objective-C: Category。
objc_ivar_list 結構體用來儲存成員變數的列表,而 objc_ivar 則是儲存了單個成員變數的資訊;同理,objc_method_list 結構體儲存著方法數組的列表,而單個方法的資訊則由 objc_method 結構體儲存。
值得注意的時,objc_class 中也有一個 isa 指標,這說明 Objc 類本身也是一個對象。為了處理類和對象的關係,Runtime 庫建立了一種叫做 Meta Class(元類) 的東西,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的中繼資料。
我們所熟悉的類方法,就源自於 Meta Class。我們可以理解為類方法就是類對象的執行個體方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。
當你發出一個類似 [NSObject alloc](類方法) 的訊息時,實際上,這個訊息被發送給了一個類對象(Class Object),這個類對象必須是一個元類的執行個體,而這個元類同時也是一個根元類(Root Meta Class)的執行個體。所有元類的 isa 指標最終都指向根元類。
所以當 [NSObject alloc] 這條訊息發送給類對象的時候,運行時代碼 objc_msgSend() 會去它元類中尋找能夠響應訊息的方法實現,如果找到了,就會對這個類對象執行方法調用。
上圖實現是 super_class 指標,虛線時 isa 指標。而根元類的父類是 NSObject,isa指向了自己。而 NSObject 沒有父類。
最後 objc_class 中還有一個 objc_cache ,緩衝,它的作用很重要,後面會提到。 Method
Method 代表類中某個方法的類型
typedef struct objc_method *Method;struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;}
objc_method 儲存了方法名,方法類型和方法實現: 方法名類型為 SEL 方法類型 method_types 是個 char 指標,儲存方法的參數類型和傳回值類型 method_imp 指向了方法的實現,本質是一個函數指標 Ivar
Ivar 是表示成員變數的類型。
typedef struct objc_ivar *Ivar;struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE;#ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif}
其中 ivar_offset 是基地址位移位元組 IMP
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個函數指標,這是由編譯器產生的。當你發起一個 ObjC 訊息之後,最終它會執行的那段代碼,就是由這個函數指標指定的。而 IMP 這個函數指標就指向了這個方法的實現。
如果得到了執行某個執行個體某個方法的入口,我們就可以繞開訊息傳遞階段,直接執行方法,這在後面 Cache 中會提到。
你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每個方法名都對應一個 SEL 類型的方法選取器,而每個執行個體對象中的 SEL 對應的方法實現肯定是唯一的,通過一組 id和 SEL 參數就能確定唯一的方法實現地址。
而一個確定的方法也只有唯一的一組 id 和 SEL 參數。 Cache
Cache 定義如下:
typedef struct objc_cache *Cachestruct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE;};
Cache 為方法調用的效能進行最佳化,每當執行個體對象接收到一個訊息時,它不會直接在 isa 指標指向的類的方法列表中遍曆尋找能夠響應的方法,因為每次都要尋找效率太低了,而是優先在 Cache 中尋找。
Runtime 系統會把被調用的方法存到 Cache 中,如果一個方法被調用,那麼它有可能今後還會被調用,下次尋找的時候就會效率更高。就像電腦群組成原理中 CPU 繞過主存先訪問 Cache 一樣。 Property
typedef struct objc_property *Property;typedef struct objc_property *objc_property_t;//這個更常用
可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法擷取類和協議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
注意:
返回的是屬性列表,列表中每個元素都是一個 objc_property_t 指標
#import <Foundation/Foundation.h>@interface Person : NSObject/** 姓名 */@property (strong, nonatomic) NSString *name;/** age */@property (assign, nonatomic) int age;/** weight */@property (