蘋果官方文檔查找地址:https://developer.apple.com/library/mac/navigation/
Runtime官方文檔https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html
簡介
Objective-C是基于C加入了面向?qū)ο筇匦院拖⑥D(zhuǎn)發(fā)機制的動態(tài)語言泪喊,除編譯器之外祖很,還需用Runtime系統(tǒng)來動態(tài)創(chuàng)建類和對象乘碑,進行消息發(fā)送和轉(zhuǎn)發(fā)。Runtime 又叫運行時奴烙,是一套底層的 C 語言 API,其為 iOS 內(nèi)部的核心之一,我們平時編寫的 OC 代碼,底層都是基于它來實現(xiàn)的努酸。比如:
OC中方法的調(diào)用:
[receiver message];
// 底層運行時會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
// 如果其還有參數(shù)比如:
[receiver message:(id)arg...];
// 底層運行時會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
id objc_msgSend ( id self, SEL op, ... );
以上你可能看不出它的價值,但是我們需要了解的是 Objective-C 是一門動態(tài)語言罩缴,它會將一些工作放在代碼運行時才處理而并非編譯時蚊逢。也就是說,有很多類和成員變量在我們編譯的時是不知道的箫章,而在運行時,我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運行镜会。
Runtime 的作用
Objc 在三種層面上與 Runtime 系統(tǒng)進行交互:
1.通過 Objective-C 源代碼
2.通過 Foundation 框架的 NSObject 類定義的方法
3.通過對 Runtime 庫函數(shù)的直接調(diào)用
Objective-C 源代碼
多數(shù)情況我們只需要編寫 OC 代碼即可檬寂,編譯時Runtime 系統(tǒng)自動在幕后搞定一切,還記得簡介中如果我們調(diào)用方法戳表,編譯器會將 OC 代碼轉(zhuǎn)換成運行時代碼桶至,在運行時確定數(shù)據(jù)結(jié)構(gòu)和函數(shù)。
通過 Foundation 框架的 NSObject 類定義的方法
Cocoa 程序中絕大部分類都是 NSObject 類的子類匾旭,所以都繼承了 NSObject 的行為镣屹。(NSProxy 類時個例外,它是個抽象超類)
一些情況下价涝,NSObject 類僅僅定義了完成某件事情的模板女蜈,并沒有提供所需要的代碼。例如 -description 方法色瘩,該方法返回類內(nèi)容的字符串表示伪窖,該方法主要用來調(diào)試程序。NSObject 類并不知道子類的內(nèi)容居兆,所以它只是返回類的名字和對象的地址覆山,NSObject 的子類可以重新實現(xiàn)。
還有一些 NSObject 的方法可以從 Runtime 系統(tǒng)中獲取信息泥栖,允許對象進行自我檢查簇宽。例如:
- -class方法返回對象的類勋篓;
- -isKindOfClass: 和 -isMemberOfClass: 方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變量);
- -respondsToSelector: 檢查對象能否響應指定的消息魏割;
- -conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法生巡;
- -methodForSelector: 返回指定方法實現(xiàn)的地址。
通過對 Runtime 庫函數(shù)的直接調(diào)用
Runtime 系統(tǒng)是具有公共接口的動態(tài)共享庫见妒。頭文件存放于/usr/include/objc目錄下孤荣,這意味著我們使用時只需要引入objc/Runtime.h頭文件即可。
許多函數(shù)可以讓你使用純 C 代碼來實現(xiàn) Objc 中同樣的功能须揣。除非是寫一些 Objc 與其他語言的橋接或是底層的 debug 工作盐股,你在寫 Objc 代碼時一般不會用到這些 C 語言函數(shù)。對于公共接口都有哪些請參考蘋果官方的 API 文檔耻卡。
一些 Runtime 的術(shù)語的數(shù)據(jù)結(jié)構(gòu)
要想全面了解 Runtime 機制疯汁,我們必須先了解 Runtime 的一些術(shù)語,他們都對應著數(shù)據(jù)結(jié)構(gòu)卵酪。
SEL
它是selector在 Objc 中的表示(Swift 中是 Selector 類)幌蚊。selector 是方法選擇器,其實作用就和名字一樣溃卡,日常生活中溢豆,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法瘸羡。selector 對方法名進行包裝漩仙,以便找到對應的方法實現(xiàn)。它的數(shù)據(jù)結(jié)構(gòu)是:
/// An opaque type that represents a method selector.(表示方法選擇器的不透明類型)
typedef struct objc_selector *SEL;
兩個類之間犹赖,不管它們是父類與子類的關(guān)系队他,還是之間沒有這種關(guān)系,只要方法名相同峻村,那么方法的SEL就是一樣的麸折。每一個方法都對應著一個SEL。所以在 Objective-C同一個類(及類的繼承體系)中粘昨,不能存在2個同名的方法垢啼,即使參數(shù)類型不同也不行。
不同類的實例對象執(zhí)行相同的selector時雾棺,會在各自的方法列表中去根據(jù)selector去尋找自己對應的IMP膊夹。
工程中的所有的SEL組成一個Set集合,Set的特點就是唯一捌浩,因此SEL是唯一的放刨。因此,如果我們想到這個方法集合中查找某個方法時尸饺,只需要去 找到這個方法對應的SEL就行了进统,SEL實際上就是根據(jù)方法名hash化了的一個字符串助币,而對于字符串的比較僅僅需要比較他們的地址就可以了。
我們可以通過以下方法獲取SEL
1.SEL sel_registerName(const char *str)//向runtime system注冊一個方法名螟碎。如果方法名已經(jīng)注冊眉菱,則放回已經(jīng)注冊的SEL
2.SEL sel_getUid(const char *str)//同上
3.@selector(<#selector#>)//oc編譯器提供的
4.SEL NSSelectorFromString(NSString *aSelectorName)//OC字符串轉(zhuǎn)化
5.SEL method_getName ( Method m );//根據(jù)Method結(jié)構(gòu)體獲取
等等
id
objc_msgSend第一個參數(shù)的數(shù)據(jù)類型id,id 是一個參數(shù)類型,它是指向某個類的實例的指針掉分,id是通用類型指針俭缓,能夠表示任何對象
typedef struct objc_object *id;
struct objc_object { Class isa; };
id其實就是一個指向objc_object結(jié)構(gòu)體指針,它包含一個Class isa成員酥郭,根據(jù)isa指針就可以順藤摸瓜找到對象所屬的類.
注意:根據(jù)Apple的官方文檔Key-Value Observing Implementation Details提及华坦,key-value observing是使用isa-swizzling的技術(shù)實現(xiàn)的,isa指針在運行時被修改不从,指向一個中間類而不是真正的類惜姐。所以,你不應該使用isa指針來確定類的關(guān)系椿息,而是使用class方法來確定實例對象的類歹袁。
Class
typedef struct objc_class *Class;
Class 其實是指向 objc_class 結(jié)構(gòu)體的指針。objc_class 的數(shù)據(jù)結(jié)構(gòu)如下:
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 可以看到寝优,一個運行時類中關(guān)聯(lián)了它的父類指針条舔、類名、成員變量倡勇、方法逞刷、緩存以及附屬的協(xié)議。
其中 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;
}
由此可見妻熊,我們可以動態(tài)修改 *methodList 的值來添加成員方法,這也是 Category 實現(xiàn)的原理仑最,同樣解釋了 Category 不能添加屬性的原因扔役。這里可以參考下美團技術(shù)團隊的文章:深入理解 Objective-C: Category。
成員變量和屬性的區(qū)別:@property聲明的屬性默認會生成一個“_”類型的成員變量警医,同時也會生成setter/getter方法亿胸。
objc_ivar_list 結(jié)構(gòu)體用來存儲成員變量的鏈表,而 objc_ivar 則是存儲了單個成員變量的信息预皇;同理侈玄,objc_method_list 結(jié)構(gòu)體存儲著方法數(shù)組的鏈表,而單個方法的信息則由 objc_method 結(jié)構(gòu)體存儲吟温。
值得注意的時序仙,objc_class 中也有一個 isa 指針,這說明 Objc 類本身也是一個對象鲁豪。為了處理類和對象的關(guān)系潘悼,Runtime 庫創(chuàng)建了一種叫做 Meta Class(元類) 的東西律秃,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數(shù)據(jù)治唤。
我們所熟悉的類方法棒动,就源自于 Meta Class。我們可以理解為類方法就是類對象的實例方法宾添。每個類僅有一個類對象船惨,而每個類對象僅有一個與之相關(guān)的元類。
當你發(fā)出一個類似 [NSObject alloc]類方法的消息時缕陕,實際上粱锐,這個消息被發(fā)送給了一個類對象(Class Object),這個類對象必須是一個元類的實例榄檬,而這個元類同時也是一個根元類(Root Meta Class)的實例卜范。所有元類的 isa 指針最終都指向根元類。
所以當 [NSObject alloc] 這條消息發(fā)送給類對象的時候鹿榜,運行時代碼 objc_msgSend() 會去它元類中查找能夠響應消息的方法實現(xiàn)海雪,如果找到了,就會對這個類對象執(zhí)行方法調(diào)用舱殿。
上圖實現(xiàn)是 super_class 指針奥裸,虛線時 isa 指針。有幾個關(guān)鍵點需要解釋以下:
- Root class (class)其實就是NSObject沪袭,NSObject是沒有超類的湾宙,所以Root class(class)的superclass指向nil。
- 每個Class都有一個isa指針指向唯一的Meta class冈绊。
- Root class(meta)的superclass指向Root class(class)侠鳄,也就是NSObject,形成一個回路死宣。
- 每個Meta class根元類的isa指針都指向Root class (meta)伟恶。
最后 objc_class 中還有一個 objc_cache ,緩存毅该,cache用來緩存經(jīng)常訪問的方法博秫,它指向objc_cache結(jié)構(gòu)體,它的作用很重要眶掌,后面會提到挡育。
IMP
IMP實際上是一個函數(shù)指針,指向方法實現(xiàn)的首地址朴爬。當你向某個對象發(fā)送一條信息即寒,可以由這個函數(shù)指針來指定方法的實現(xiàn),它最終就會執(zhí)行那段代碼,這樣可以繞開消息傳遞階段而去執(zhí)行另一個方法實現(xiàn)蒿叠。其定義如下:
typedef id (*IMP)(id, SEL, ...);
第一個參數(shù)是指向self的指針(如果是實例方法明垢,則是類實例的內(nèi)存地址;如果是類方法市咽,則是指向元類的指針)痊银,第二個參數(shù)是方法選擇器(selector),接下來是方法的實際參數(shù)列表施绎。
IMP原理:SEL是方法名的唯一標識溯革,相同的方法名如果要對應不同的實現(xiàn)還需關(guān)聯(lián)不同的實現(xiàn)地址,IMP就是為此而生的谷醉。當我們知道一個IMP時候就會知道他對應的SEL名致稀。
IMP的作用:
我們就可以像調(diào)用普通的C語言函數(shù)一樣來使用這個函數(shù)指針 了。
通過取得IMP俱尼,我們可以跳過Runtime的消息傳遞機制抖单,直接執(zhí)行IMP指向的函數(shù)實現(xiàn),這樣省去了Runtime消息傳遞過程中所做的一系列查找操作遇八,會比直接向?qū)ο蟀l(fā)送消息高效一些矛绘。
獲取IMP的函數(shù)如下
IMP imp_implementationWithBlock(id block)//根據(jù)代碼塊獲取IMP,其實就是代碼塊與IMP關(guān)聯(lián)
IMP method_getImplementation(Method m) //根據(jù)Method獲取IMP
[[objc Class] instanceMethodForSelector:SEL]//根據(jù)OC方式獲取IMP
//待續(xù)
當我們獲取一個方法的IMP時候可以直接調(diào)用IMP
IMP imp = method_getImplementation(Method m);
id objc = imp(id,SEL,argument);//objc用來保存方法的返回值刃永,id表示調(diào)用這個方法的對象货矮,SEL是Method的選擇器,argument是方法的參數(shù)斯够。
注意:
1.對于一個對象方法其實他的第一個參數(shù)是對象本身,第二個參數(shù)開始被存放在IMP的…里面囚玫。所以對于不屬于任何對象的IMP我們要直接調(diào)用的參數(shù)是從IMP的第一參數(shù)id算起,忽略SEL傳nil读规,然后接著后面的第二第三個參數(shù)抓督。對于對象方法(包括類方法,類方法對象其實就是元類)則我們可以對id和sel直接傳nil束亏,忽略他然后直接調(diào)用該對象方法達到像使用C函數(shù)一樣使用對象方法本昏。
2.在OC里面對象方法是至少兩個參數(shù)的C函數(shù),兩個參數(shù)是背隱藏起來的,即對象的自身self和方法名_cmd,我們在開發(fā)中常用到self枪汪,但是很少用到_cmd。
3.用imp掉用方法時候參數(shù)一點要傳全,沒有值的可以傳nil怔昨,否則會出現(xiàn)錯誤或者一些想不到的結(jié)果雀久。
Method
Method定義如下:它主要是用于描述類里面的方法,代表類中某個方法的類型
typedef struct objc_method *Method;
objc_method結(jié)構(gòu)體定義如下:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;//方法名
char *method_types OBJC2_UNAVAILABLE;//參數(shù)返回值字符串描述
IMP method_imp OBJC2_UNAVAILABLE;//方法的實現(xiàn)
}
objc_method 存儲了方法名趁舀,方法類型和方法實現(xiàn):
方法名 method_name 類型為 SEL
方法類型 method_types 是個 char 指針赖捌,存儲方法的參數(shù)類型和返回值類型
method_imp 指向了方法的實現(xiàn),本質(zhì)是一個函數(shù)指針
我們可以看到該結(jié)構(gòu)體中包含一個SEL和IMP,實際上相當于在SEL和IMP之間作了一個映射越庇。有了Method罩锐,SEL可以通過Method找到對應的IMP,從而調(diào)用方法的實現(xiàn)代碼卤唉。
Method操作函數(shù)如下
方法操作主要有以下函數(shù):
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數(shù)組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實現(xiàn)
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具體實現(xiàn)
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 類實例是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
objc_method_description
objc_method_description結(jié)構(gòu)體用來描述Method的一些信息涩惑,這樣我么就可以直接讀出來
struct objc_method_description {
SEL name; /**< The name of the method 方法名*/
char *types; /**< The types of the method arguments 參數(shù)類型字符串*/
};
獲取函數(shù)
// 返回指定方法的方法描述結(jié)構(gòu)體
struct objc_method_description * method_getDescription ( Method m );
Ivar
Ivar表示類中的實例變量,在runtime.h文件中找到它的定義:
/// An opaque type that represents an instance variable.
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其實就是一個指向objc_ivar結(jié)構(gòu)體指針桑驱,它包含了變量名(ivar_name)竭恬、變量類型(ivar_type)等信息。
Cache
顧名思義,Cache主要用來緩存,那它緩存什么呢钞翔?我們先在runtime.h文件看看它的定義:
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache其實就是一個存儲Method的鏈表胯杭,主要是為了優(yōu)化方法調(diào)用的性能。當對象receiver調(diào)用方法message時秦忿,首先根據(jù)對象receiver的isa指針查找到它對應的類,然后在類的methodLists中搜索方法,如果沒有找到盒揉,就使用super_class指針到父類中的methodLists查找,一旦找到就調(diào)用方法骑歹。如果沒有找到预烙,有可能消息轉(zhuǎn)發(fā),也可能忽略它道媚。但這樣查找方式效率太低扁掸,因為往往一個類大概只有20%的方法經(jīng)常被調(diào)用,占總調(diào)用次數(shù)的80%最域。所以使用Cache來緩存經(jīng)常調(diào)用的方法谴分,當調(diào)用方法時,優(yōu)先在Cache查找镀脂,如果沒有找到牺蹄,再到methodLists查找。
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更常用
可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協(xié)議中的屬性:
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 (assign, nonatomic) double weight;
@end
以上是一個 Person 類沙兰,有3個屬性。讓我們用上述方法獲取類的運行時屬性翘魄。
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
NSLog(@"%d", outCount);
for (NSInteger i = 0; i < outCount; i++) {
NSString *name = @(property_getName(properties[i]));
NSString *attributes = @(property_getAttributes(properties[i]));
NSLog(@"%@--------%@", name, attributes);
}
打印結(jié)果如下:
2017-02-13 16:54:08.684 Runtime體驗[7435:176645] 3
2017-02-13 16:54:08.684 Runtime體驗[7435:176645] name--------T@"NSString",&,N,V_name
2017-02-13 16:54:08.684 Runtime體驗[7435:176645] age--------Ti,N,V_age
2017-02-13 16:54:08.684 Runtime體驗[7435:176645] weight--------Td,N,V_weight
property_getName 用來查找屬性的名稱鼎天,返回 c 字符串。property_getAttributes 函數(shù)挖掘?qū)傩缘恼鎸嵜Q和 @encode 類型暑竟,返回 c 字符串斋射。
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
class_getProperty 和 protocol_getProperty 通過給出屬性名在類和協(xié)議中獲得屬性的引用