摘要:這篇文章首先介紹runtime原理,包括類,超類,元類,super_class,isa,物件,方法,SEL,IMP等概念,同時分別介紹與這些概念有關的API。接著介紹方法呼叫流程,以及尋找IMP的過程。然後,介紹一下這些API的常見用法,並介紹runtime的冷門知識。最後介紹一下runtime的實戰指南。
Tips:蘋果公開的原始碼在這裡可以查,opensource.apple.com/tarballs/
例如,其中,有兩個比較常見需要學習原始碼的地址:
- runtime的原始碼在opensource.apple.com/tarballs/ob…
- runloop(其實是整個 CoreFoundation)的原始碼在opensource.apple.com/tarballs/CF…
當然,如果你想在github上線上檢視原始碼,可以點這裡:runtime,runloop
1. 執行時
1.1 基本概念: 執行時
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 庫函式的直接呼叫
1.2 各種基本概念的C表達
在 Objective-C 中,類、物件和方法都是一個 C 的結構體,從 objc/objc.h
(物件,objc_object
,id
)以及objc/runtime.h
(其它,類,方法,方法列表,變數列表,屬性列表等相關的)以及中,我們可以找到他們的定義。
① 類
類物件(Class)是由程式設計師定義並在執行時由編譯器建立的,它沒有自己的例項變數,這裡需要注意的是類的成員變數和例項方法列表是屬於例項物件的,但其儲存於類物件當中的。我們在objc/objc.h
下看看Class的定義:
- Class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
複製程式碼
可以看到類是由Class型別來表示的,它是一個objc_class
結構型別的指標。我們接著來看objc_class
結構體的定義:
- objc_class
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
複製程式碼
引數解析
- isa指標是和Class同型別的objc_class結構指標,類物件的指標指向其所屬的類,即元類。元類中儲存著類物件的類方法,當訪問某個類的類方法時會通過該isa指標從元類中尋找方法對應的函式指標。
- super_class指標指向該類所繼承的父類物件,如果該類已經是最頂層的根類(如NSObject或NSProxy), 則 super_class為NULL。
-
cache:用於快取最近使用的方法。一個接收者物件接收到一個訊息時,它會根據
isa
指標去查詢能夠響應這個訊息的物件。在實際使用中,這個物件只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次訊息來時,我們都是methodLists
中遍歷一遍,效能勢必很差。這時,cache就派上用場了。在我們每次呼叫過一個方法後,這個方法就會被快取到cache列表中,下次呼叫的時候runtime就會優先去cache中查詢,如果cache沒有,才去methodLists
中查詢方法。這樣,對於那些經常用到的方法的呼叫,但提高了呼叫的效率。 -
version:我們可以使用這個欄位來提供類的版本資訊。這對於物件的序列化非常有用,它可是讓我們識別出不同類定義版本中例項變數佈局的改變。
-
protocols:當然可以看出這一個
objc_protocol_list
的指標。關於objc_protocol_list
的結構體構成後面會講。
獲取類的類名
// 獲取類的類名
const char * class_getName ( Class cls );
複製程式碼
動態建立類
// 建立一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes ); //如果建立的是root class,則superclass為Nil。extraBytes通常為0
// 銷燬一個類及其相關聯的類
void objc_disposeClassPair ( Class cls ); //在執行中還存在或存在子類例項,就不能夠呼叫這個。
// 在應用中註冊由objc_allocateClassPair建立的類
void objc_registerClassPair ( Class cls ); //建立了新類後,然後使用class_addMethod,class_addIvar函式為新類新增方法,例項變數和屬性後再呼叫這個來註冊類,再之後就能夠用了。
複製程式碼
② 物件
例項物件是我們對類物件alloc
或者new
操作時所建立的,在這個過程中會拷貝例項所屬的類的成員變數,但並不拷貝類定義的方法。呼叫例項方法時,系統會根據例項的isa
指標去類的方法列表及父類的方法列表中尋找與訊息對應的selector
指向的方法。同樣的,我們也來看下其定義:
- objc_object
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
複製程式碼
可以看到,這個結構體只有一個isa
變數,指向例項物件所屬的類。任何帶有以指標開始並指向類結構的結構都可以被視作objc_object
, 物件最重要的特點是可以給其傳送訊息。 NSObject類的alloc
和allocWithZone:
方法使用函式class_createInstance
來建立objc_object
資料結構。
另外我們常見的id
型別,它是一個objc_object
結構型別的指標。該型別的物件可以轉換為任何一種物件,類似於C語言中void *
指標型別的作用。其定義如下所示:
- id
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
複製程式碼
對物件的類操作
// 返回給定物件的類名
const char * object_getClassName ( id obj );
// 返回物件的類
Class object_getClass ( id obj );
// 設定物件的類
Class object_setClass ( id obj, Class cls );
複製程式碼
獲取物件的類定義
// 獲取已註冊的類定義的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 建立並返回一個指向所有已註冊類的指標列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定類的類定義
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定類的元類
Class objc_getMetaClass ( const char *name );
複製程式碼
動態建立物件
// 建立類例項
id class_createInstance ( Class cls, size_t extraBytes ); //會在heap裡給類分配記憶體。這個方法和+alloc方法類似。
// 在指定位置建立類例項
id objc_constructInstance ( Class cls, void *bytes );
// 銷燬類例項
void * objc_destructInstance ( id obj ); //不會釋放移除任何相關引用
複製程式碼
③ 元類
元類(Metaclass)就是類物件的類,每個類都有自己的元類,也就是objc_class
結構體裡面isa
指標所指向的類. Objective-C的類方法是使用元類的根本原因,因為其中儲存著對應的類物件呼叫的方法即類方法。
當向物件發訊息,runtime會在這個物件所屬類方法列表中查詢傳送訊息對應的方法,但當向類傳送訊息時,runtime就會在這個類的meta class方法列表裡查詢。所有的meta class,包括Root class,Superclass,Subclass的isa都指向Root class的meta class,這樣能夠形成一個閉環。
所以由上圖可以看到,在給例項物件或類物件傳送訊息時,尋找方法列表的規則為:
- 當傳送訊息給例項物件時,訊息是在尋找這個物件的類的方法列表(例項方法)
- 當傳送訊息給類物件時,訊息是在尋找這個類的元類的方法列表(類方法)
元類,就像之前的類一樣,它也是一個物件,也可以呼叫它的方法。所以這就意味著它必須也有一個類。所有的元類都使用根元類作為他們的類。比如所有NSObject的子類的元類都會以NSObject的元類作為他們的類。
根據這個規則,所有的元類使用根元類作為他們的類,根元類的元類則就是它自己。也就是說基類的元類的isa指標指向他自己。
操作函式
- super_class和meta-class
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 判斷給定的Class是否是一個meta class
BOOL class_isMetaClass ( Class cls );
複製程式碼
- instance_size
// 獲取例項大小
size_t class_getInstanceSize ( Class cls );
複製程式碼
④ 屬性
在Objective-C中,屬性(property)和成員變數是不同的。那麼,屬性的本質是什麼?它和成員變數之間有什麼區別?簡單來說屬性是新增了存取方法的成員變數,也就是:
@property = ivar + getter + setter;
因此,我們每定義一個@property都會新增對應的ivar, getter和setter到類結構體objc_class
中。具體來說,系統會在objc_ivar_list
中新增一個成員變數的描述,然後在methodLists
中分別新增setter和getter方法的描述。下面的objc_property_t
是宣告的屬性的型別,是一個指向objc_property結構體的指標。
用法舉例
//遍歷獲取所有屬性Property
- (void) getAllProperty {
unsigned int propertyCount = 0;
objc_property_t *propertyList = class_copyPropertyList([Person class], &propertyCount);
for (unsigned int i = 0; i < propertyCount; i++ ) {
objc_property_t *thisProperty = propertyList[i];
const char* propertyName = property_getName(*thisProperty);
NSLog(@"Person擁有的屬性為: '%s'", propertyName);
}
}
複製程式碼
- objc_property_t
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
複製程式碼
另外,關於屬性有一個objc_property_attribute_t
結構體列表,objc_property_attribute_t
結構體包含name
和value
- objc_property_attribute_t
typedef struct {
const char * _Nonnull name; /**< The name of the attribute */
const char * _Nonnull value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
複製程式碼
常用的屬性如下:
- 屬性型別 name值:T value:變化
- 編碼型別 name值:C(copy) &(strong) W(weak)空(assign) 等 value:無
- 非/原子性 name值:空(atomic) N(Nonatomic) value:無
- 變數名稱 name值:V value:變化
例如
@interface person : NSObjec{
NSString *_name;
}
int main(){
objc_property_attribute_t nonatomic = {"N", ""};
objc_property_attribute_t strong = {"&", ""};
objc_property_attribute_t type = {"T", "@\"NSString\""};
objc_property_attribute_t ivar = {"V", "_name"};
objc_property_attribute_t attributes[] = {nonatomic, strong, type, ivar};
BOOL result = class_addProperty([person class], "name", attributes, 4);
}
複製程式碼
操作函式
// 獲取屬性名
const char * property_getName ( objc_property_t property );
// 獲取屬性特性描述字串
const char * property_getAttributes ( objc_property_t property );
// 獲取屬性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 獲取屬性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
複製程式碼
⑤ 成員變數
Ivar: 例項變數型別,是一個指向objc_ivar
結構體的指標
- Ivar
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
複製程式碼
objc_ivar
結構體的組成如下:
- objc_ivar
- struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
複製程式碼
這裡我們注意第三個成員 ivar_offset
。它表示基地址偏移位元組。
操作函式
//成員變數操作函式
// 修改類例項的例項變數的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 獲取物件例項變數的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 返回指向給定物件分配的任何額外位元組的指標
void * object_getIndexedIvars ( id obj );
// 返回物件中例項變數的值
id object_getIvar ( id obj, Ivar ivar );
// 設定物件中例項變數的值
void object_setIvar ( id obj, Ivar ivar, id value );
// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );
// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types ); //這個只能夠向在runtime時建立的類新增成員變數
// 獲取整個成員變數列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount ); //必須使用free()來釋放這個陣列
複製程式碼
⑥ 成員變數列表
在objc_class
中,所有的成員變數、屬性的資訊是放在連結串列ivars
中的。ivars
是一個陣列,陣列中每個元素是指向Ivar
(變數資訊)的指標。
- objc_ivar_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;
}
複製程式碼
例子,獲取所有成員變數
//遍歷獲取Person類所有的成員變數IvarList
- (void) getAllIvarList {
unsigned int methodCount = 0;
Ivar * ivars = class_copyIvarList([Person class], &methodCount);
for (unsigned int i = 0; i < methodCount; i ++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
const char * type = ivar_getTypeEncoding(ivar);
NSLog(@"Person擁有的成員變數的型別為%s,名字為 %s ",type, name);
}
free(ivars);
}
複製程式碼
⑦ 方法
Method 代表類中某個方法的型別
- Method
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
複製程式碼
objc_method 儲存了方法名,方法型別和方法實現:
- objc_method
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
複製程式碼
其中,
- 方法名型別為 SEL
- 方法型別 method_types 是個 char 指標,儲存方法的引數型別和返回值型別
- method_imp 指向了方法的實現,本質是一個函式指標
簡言之,Method = SEL + IMP + method_types,相當於在SEL和IMP之間建立了一個對映。
操作函式
// 呼叫指定方法的實現,返回的是方法實現時的返回,引數receiver不能為空,這個比method_getImplementation和method_getName快
id method_invoke ( id receiver, Method m, ... );
// 呼叫返回一個資料結構的方法的實現
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名,希望獲得方法明的C字串,使用sel_getName(method_getName(method))
SEL method_getName ( Method m );
// 返回方法的實現
IMP method_getImplementation ( Method m );
// 獲取描述方法引數和返回值型別的字串
const char * method_getTypeEncoding ( Method m );
// 獲取方法的返回值型別的字串
char * method_copyReturnType ( Method m );
// 獲取方法的指定位置引數的型別字串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通過引用返回方法的返回值型別字串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的引數的個數
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置引數的型別字串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述結構體
struct objc_method_description * method_getDescription ( Method m );
// 設定方法的實現
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個方法的實現
void method_exchangeImplementations ( Method m1, Method m2 );
複製程式碼
⑧ 方法列表
方法呼叫是通過查詢物件的isa指標所指向歸屬類中的methodLists來完成。
- objc_method_list
struct objc_method_list {
struct objc_method_list * _Nullable 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;
}
複製程式碼
操作函式
// 新增方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types ); //和成員變數不同的是可以為類動態新增方法。如果有同名會返回NO,修改的話需要使用method_setImplementation
// 獲取例項方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的陣列
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 類例項是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
複製程式碼
⑨ 指標:指向方法名與實現
- SEL
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
複製程式碼
在原始碼中沒有直接找到 objc_selector
的定義,從一些書籍上與 Blog 上看到可以將 SEL 理解為一個 char*
指標。
具體這 objc_selector
結構體是什麼取決與使用GNU的還是Apple的執行時, 在Mac OS X中SEL其實被對映為一個C字串,可以看作是方法的名字,它並不一個指向具體方法實現(IMP型別才是)。
對於所有的類,只要方法名是相同的,產生的selector都是一樣的。
操作函式
// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系統中註冊一個方法,將方法名對映到一個選擇器,並返回這個選擇器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系統中註冊一個方法
SEL sel_getUid ( const char *str );
// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
複製程式碼
- IMP
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
複製程式碼
實際上就是一個函式指標,指向方法實現的首地址。通過取得 IMP,我們可以跳過 runtime 的訊息傳遞機制,直接執行 IMP指向的函式實現,這樣省去了 runtime 訊息傳遞過程中所做的一系列查詢操作,會比直接向物件傳送訊息高效一些,當然必須說明的是,這種方式只適用於極特殊的優化場景,如效率敏感的場景下大量迴圈的呼叫某方法。
操作函式
// 返回方法的實現
IMP method_getImplementation ( Method m );
// 設定方法的實現
IMP method_setImplementation ( Method m, IMP imp );
// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
複製程式碼
⑩ 快取
上面提到了objc_class結構體中的cache欄位,它用於快取呼叫過的方法。這個欄位是一個指向objc_cache結構體的指標,其定義如下:
- Cache
typedef struct objc_cache *Cache
複製程式碼
- objc_cache
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
#define CACHE_BUCKET_NAME(B) ((B)->method_name)
#define CACHE_BUCKET_IMP(B) ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))
#endif
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
複製程式碼
該結構體的欄位描述如下:
- mask:一個整數,指定分配的快取bucket的總數。在方法查詢過程中,Objective-C runtime使用這個欄位來確定開始線性查詢陣列的索引位置。指向方法selector的指標與該欄位做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash雜湊演算法。
- occupied:一個整數,指定實際佔用的快取bucket的總數。
- buckets:指向Method資料結構指標的陣列。這個陣列可能包含不超過mask+1個元素。需要注意的是,指標可能是NULL,表示這個快取bucket沒有被佔用,另外被佔用的bucket可能是不連續的。這個陣列可能會隨著時間而增長。
⑪ 協議連結串列
前面objc_class
的結構體中有個協議連結串列的引數,協議連結串列用來儲存宣告遵守的正式協議
- objc_protocol_list
struct objc_protocol_list {
struct objc_protocol_list * _Nullable next;
long count;
__unsafe_unretained Protocol * _Nullable list[1];
};
複製程式碼
操作函式
// 新增協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
// 返回指定的協議
Protocol * objc_getProtocol ( const char *name );
// 獲取執行時所知道的所有協議的陣列
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 建立新的協議例項
Protocol * objc_allocateProtocol ( const char *name );
// 在執行時中註冊新建立的協議
void objc_registerProtocol ( Protocol *proto ); //建立一個新協議後必須使用這個進行註冊這個新協議,但是註冊後不能夠再修改和新增新方法。
// 為協議新增方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 新增一個已註冊的協議到協議中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 為協議新增屬性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回協議名
const char * protocol_getName ( Protocol *p );
// 測試兩個協議是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 獲取協議中指定條件的方法的方法描述陣列
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 獲取協議中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 獲取協議中的屬性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 獲取協議的指定屬性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 獲取協議採用的協議
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 檢視協議是否採用了另一個協議
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
複製程式碼
⑫ 分類
- Category
/// An opaque type that represents a category.
typedef struct objc_category *Category;
複製程式碼
- objc_category
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
複製程式碼
2. 方法呼叫流程
objc_msgSend() Tour 系列文章通過對 objc_msgSend
的彙編原始碼分析,總結出以下流程:
2.1 方法呼叫流程
- 檢查 selector 是否需要忽略
- 檢查 target 是否為 nil,如果是 nil 就直接 cleanup,然後 return
- 在 target 的 Class 中根據 selector 去找 IMP
2.2 尋找 IMP 的過程:
- 在當前 class 的方法快取裡尋找(cache methodLists)
- 找到了跳到對應的方法實現,沒找到繼續往下執行
- 從當前 class 的 方法列表裡查詢(methodLists),找到了新增到快取列表裡,然後跳轉到對應的方法實現;沒找到繼續往下執行
- 從 superClass 的快取列表和方法列表裡查詢,直到找到基類為止
- 以上步驟還找不到 IMP,則進入訊息動態處理和訊息轉發流程,詳見這篇文章
我們能在 objc4官方原始碼 中找到上述尋找 IMP 的過程,具體對應的程式碼如下:
objc-class.mm
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
複製程式碼
objc-runtime-new.mm
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
複製程式碼
等等...
3. 執行時相關的API
3.1 通過 Foundation 框架的 NSObject 類定義的方法
Cocoa 程式中絕大部分類都是 NSObject 類的子類,所以都繼承了 NSObject 的行為。(NSProxy 類時個例外,它是個抽象超類)
一些情況下,NSObject 類僅僅定義了完成某件事情的模板,並沒有提供所需要的程式碼。例如 -description 方法,該方法返回類內容的字串表示,該方法主要用來除錯程式。NSObject 類並不知道子類的內容,所以它只是返回類的名字和物件的地址,NSObject 的子類可以重新實現。
還有一些 NSObject 的方法可以從 Runtime 系統中獲取資訊,允許物件進行自我檢查。例如:
-class
方法返回物件的類;-isKindOfClass:
和-isMemberOfClass:
方法檢查物件是否存在於指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變數);-respondsToSelector:
檢查物件能否響應指定的訊息;-conformsToProtocol:
檢查物件是否實現了指定協議類的方法;-methodForSelector:
返回指定方法實現的地址。
常見的一個例子:
//先呼叫respondsToSelector:來判斷一下
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
複製程式碼
3.2 runtime的常見API舉例
- 獲取列表及名字
unsigned int count;
//獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
//獲取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//獲取成員變數列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
//獲取協議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
複製程式碼
- 動態建立類
Class cls = objc_allocateClassPair(MyClass.class, "MySubClass", 0);
class_addMethod(cls, @selector(submethod1), (IMP)imp_submethod1, "v@:");
class_replaceMethod(cls, @selector(method1), (IMP)imp_submethod1, "v@:");
class_addIvar(cls, "_ivar1", sizeof(NSString *), log(sizeof(NSString *)), "i");
objc_property_attribute_t type = {"T", "@\"NSString\""};
objc_property_attribute_t ownership = { "C", "" };
objc_property_attribute_t backingivar = { "V", "_ivar1"};
objc_property_attribute_t attrs[] = {type, ownership, backingivar};
class_addProperty(cls, "property2", attrs, 3);
objc_registerClassPair(cls);
id instance = [[cls alloc] init];
[instance performSelector:@selector(submethod1)];
[instance performSelector:@selector(method1)];
複製程式碼
輸出結果
2014-10-23 11:35:31.006 RuntimeTest[3800:66152] run sub method 1
2014-10-23 11:35:31.006 RuntimeTest[3800:66152] run sub method 1
複製程式碼
- 動態建立物件
//可以看出class_createInstance和alloc的不同
id theObject = class_createInstance(NSString.class, sizeof(unsigned));
id str1 = [theObject init];
NSLog(@"%@", [str1 class]);
id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);
複製程式碼
輸出結果
2014-10-23 12:46:50.781 RuntimeTest[4039:89088] NSString
2014-10-23 12:46:50.781 RuntimeTest[4039:89088] __NSCFConstantString
複製程式碼
3.3 runtime的少見API解析
id
和void *
轉換API:(__bridge void *)
在 ARC 有效時,通過 (__bridge void *)
轉換 id
和 void *
就能夠相互轉換。為什麼轉換?這是因為objc_getAssociatedObject
的引數要求的。先看一下它的API:
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
複製程式碼
可以知道,這個“屬性名”的key是必須是一個void *
型別的引數。所以需要轉換。關於這個轉換,下面給一個轉換的例子:
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
複製程式碼
關於這個轉換可以瞭解更多:ARC 型別轉換:顯示轉換 id 和 void *
當然,如果不通過轉換使用這個API,就需要這樣使用:
- 方式1:
objc_getAssociatedObject(self, @"AddClickedEvent");
複製程式碼
- 方式2:
static const void *registerNibArrayKey = ®isterNibArrayKey;
複製程式碼
NSMutableArray *array = objc_getAssociatedObject(self, registerNibArrayKey);
複製程式碼
- 方式3:
static const char MJErrorKey = '\0';
複製程式碼
objc_getAssociatedObject(self, &MJErrorKey);
複製程式碼
- 方式4:
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
{
MJProperty *propertyObj = objc_getAssociatedObject(self, property);
//省略
}
複製程式碼
其中objc_property_t
是runtime的型別
typedef struct objc_property *objc_property_t;
複製程式碼
4. 執行時實戰指南
上面的API不是提供大家背的,而是用來查閱的,當你要用到的時候查閱。因為這些原理和API光看沒用,需要實戰之後再回過頭來查閱和理解。筆者另外寫了runtime的原理與實踐。如果想了解runtime的更多知識,可以選擇閱讀這些文章: