我們都知道Objective-C 是一門動態(tài)語言荡陷,這就意味著它是一類在運行時可以改變其結(jié)構(gòu)的語言湾戳,你可以按需要把消息重定向給合適的對象,甚至可以交換方法的實現(xiàn)韧衣。我們知道方法調(diào)用的本質(zhì)就是對象發(fā)送消息盅藻,比如:[object message]
實際上被編譯器轉(zhuǎn)化成了:objc_msgSend(object, selector)
一. 動態(tài)特性
首先我們來了解一下動態(tài)特性可以大致分為動態(tài)類型(Dynamic typing),動態(tài)綁定(Dynamic binding)和動態(tài)加載(Dynamic loading)畅铭。
- 動態(tài)類型:
即是運行時才決定對象的類型氏淑,比如我們常用的id類型。這里需要說到幾個方法:
-isMemberOfClass:
是 NSObject
的方法硕噩,用以確定某個 NSObject
對象是否是某個類的成員假残。
-isKindOfClass:
可以用以確定某個對象是否是某個類或其子類的成員。
respondsToSelector:
檢查對象能否響應(yīng)指定的消息炉擅。
conformsToProtocol:
檢查對象是否實現(xiàn)了指定協(xié)議類的方法辉懒。
methodForSelector:
返回指定方法的函數(shù)指針。
- 動態(tài)綁定
基于動態(tài)類型谍失,在某個實例對象被確定后耗帕,其類型便被確定了。該對象對應(yīng)的屬性和響應(yīng)的消息也被完全確定袱贮,這就是動態(tài)綁定。傳統(tǒng)的函數(shù)一般在編譯時就已經(jīng)把參數(shù)信息和函數(shù)實現(xiàn)打包到編譯后的源碼中了,而在OC中使用的是消息機制攒巍。調(diào)用一個實例方法嗽仪,其實是向該實例的指針發(fā)送消息,實例在收到消息之后柒莉,會從自身的實現(xiàn)中去尋找響應(yīng)這條消息的方法闻坚。而動態(tài)綁定所做的,就是在實例所屬類確定后兢孝,將某些屬性和相應(yīng)的方法綁定到實例上窿凤。
- 動態(tài)加載
根據(jù)需求加載所需要的資源,比如不同設(shè)備加載不同尺寸圖片跨蟹。
二. 具體結(jié)構(gòu)
//objc/runtime.h
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指針
#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;//指向objc_method_list指針的指針
struct objc_cache *cache OBJC2_UNAVAILABLE;//方法緩存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//協(xié)議鏈表
#endif
} OBJC2_UNAVAILABLE;
//objc/objc.h
// Class其實是一個指向objc_class結(jié)構(gòu)體的指針
typedef struct objc_class *Class;
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY; //isa指針
};
//指向一個類實例的指針
typedef struct objc_object *id;
這么看來雳殊,類和對象都是一樣的結(jié)構(gòu),內(nèi)部都包含一個isa
對象窗轩,那么類本身也是一個對象夯秃。
為了處理類和對象的關(guān)系,runtime 引入了元類 (Meta Class) 痢艺,類對象所屬的類型就叫做元類仓洼,它用來表述類對象本身所具備的元數(shù)據(jù)。類方法就定義于此堤舒,因為這些方法可以理解成類對象的實例方法色建。每個類僅有一個類對象,而每個類對象僅有一個與之相關(guān)的元類舌缤。當對象的實例方法調(diào)用時箕戳,通過對象的 isa 在類中獲取方法的實現(xiàn)。類對象的類方法調(diào)用時友驮,通過類的 isa 在元類中獲取方法的實現(xiàn)漂羊。
當你發(fā)出一個類似[NSObject alloc]
的消息時,你事實上是把這個消息發(fā)給了一個類對象 (Class Object) 卸留,這個類對象必須是一個元類的實例走越,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類耻瑟。所有的元類的方法列表都有能夠響應(yīng)消息的類方法旨指。所以當 [NSObject alloc]
這條消息發(fā)給類對象的時候,objc_msgSend()
會去它的元類里面去查找能夠響應(yīng)消息的方法喳整,如果找到了谆构,然后對這個類對象執(zhí)行方法調(diào)用。
其他關(guān)鍵字:
1. SEL
SEL又叫選擇器框都,是表示一個方法的selector的指針搬素,其定義如下:
typedef struct objc_selector *SEL;
方法的selector用于表示運行時方法的名字。Objective-C在編譯時熬尺,會依據(jù)每一個方法的名字摸屠、參數(shù)序列,生成一個唯一的整型標識(Int類型的地址)粱哼,這個標識就是SEL季二。
兩個類之間,只要方法名相同揭措,那么方法的SEL就是一樣的胯舷,每一個方法都對應(yīng)著一個SEL。所以在Objective-C同一個類(及類的繼承體系)中绊含,不能存在2個同名的方法桑嘶,即使參數(shù)類型不同也不行
如在某一個類中定義以下兩個方法會報錯
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
當然,不同的類可以擁有相同的selector艺挪,這個沒有問題不翩。不同類的實例對象執(zhí)行相同的selector時,會在各自的方法列表中去根據(jù)selector尋找自己對應(yīng)的IMP麻裳。
工程中的所有的SEL組成一個Set集合口蝠,如果我們想到這個方法集合中查找某個方法時,只需要去找到這個方法對應(yīng)的SEL就行了津坑,SEL實際上就是根據(jù)方法名hash化了的一個字符串妙蔗,而對于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比疆瑰!
本質(zhì)上眉反,SEL只是一個指向方法的指針(準確的說,只是一個根據(jù)方法名hash化了的KEY值穆役,能唯一代表一個方法)寸五,它的存在只是為了加快方法的查詢速度。
通過下面三種方法可以獲取SEL:
a耿币、sel_registerName函數(shù)
b梳杏、Objective-C編譯器提供的@selector()
c、NSSelectorFromString()方法
2. 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; // 方法實現(xiàn)
}
我們可以看到該結(jié)構(gòu)體中包含一個SEL和IMP淹接,實際上相當于在SEL和IMP之間作了一個映射十性。有了SEL,我們便可以找到對應(yīng)的IMP塑悼,從而調(diào)用方法的實現(xiàn)代碼劲适。
3. IMP
IMP實際上是一個函數(shù)指針,指向方法實現(xiàn)的地址厢蒜。
id (*IMP)(id, SEL,...)
第一個參數(shù):是指向self的指針(如果是實例方法霞势,則是類實例的內(nèi)存地址烹植;如果是類方法,則是指向元類的指針)
第二個參數(shù):是方法選擇器(selector)
接下來的參數(shù):方法的參數(shù)列表支示。
4. 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
} OBJC2_UNAVAILABLE;
可以根據(jù)實例查找其在類中的名字,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌颂鸿!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
class_copyIvarList
函數(shù)獲取的不僅有實例變量,還有屬性攒庵。但會在原本的屬性名前加上一個下劃線嘴纺。
class_copyPropertyList
函數(shù)只能獲取類的屬性。
5. Cache
Cache
為方法調(diào)用的性能進行優(yōu)化浓冒,通俗地講栽渴,每當實例對象接收到一個消息時,它不會直接在isa
指向的類的方法列表中遍歷查找能夠響應(yīng)消息的方法稳懒,因為這樣效率太低了闲擦,而是優(yōu)先在Cache
中查找。
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;
};
6. _cmd
_cmd在Objective-C的方法中表示當前方法的selector场梆,正如同self表示當前方法調(diào)用的對象實例墅冷。
三. 調(diào)用流程
1. 消息傳遞
消息直到運行時才綁定到方法實現(xiàn)上。編譯器會將消息表達式[receiver message]轉(zhuǎn)化為一個消息函數(shù)的調(diào)用或油,即objc_msgSend寞忿。這個函數(shù)將消息接收者和方法名作為其基礎(chǔ)參數(shù),如以下所示
objc_msgSend(receiver, selector)
如果消息中還有其它參數(shù)顶岸,則該方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2,...)
另方法列表objc_method_list
本質(zhì)上是一個裝載 objc_method
元素的可變長度的數(shù)組腔彰。一個 objc_method
結(jié)構(gòu)體中包含函數(shù)名,也就是SEL辖佣,表示函數(shù)類型的字符串 (見 Type Encoding) 霹抛,以及函數(shù)的實現(xiàn)IMP。
比如:調(diào)用[obj foo];
- 首先是轉(zhuǎn)換成
objc_msgSend(obj, foo)
,通過對象的isa指針獲取到類的結(jié)構(gòu)體卷谈,再從當前class的cache方法列表(cache methodLists)里去找杯拐。 - 如果找到對應(yīng)的selector,則實現(xiàn)IMP雏搂;未找到則在 class 的 method list 找 foo 藕施。
- 如果 class 中沒到 foo,繼續(xù)往它的 superclass 中找 凸郑,即objc_msgSend結(jié)構(gòu)體中的指向父類的指針找到其父類裳食,并在父類的分發(fā)表里面查找方法的selector。
- 依次沿著類的繼承體系到NSObject芙沥,一旦找到 foo 這個函數(shù)诲祸,就去執(zhí)行它的實現(xiàn)IMP 浊吏。并把 foo 的
method_name
作為 key ,method_imp
作為 value 存進cache - 如果都未找到救氯,則會走消息轉(zhuǎn)發(fā)流程
2. 消息轉(zhuǎn)發(fā)
我們知道找田,當對象發(fā)送一個消息而沒有實現(xiàn)該方法時,編譯器會報如下錯誤:
unrecognized selector send to instance XXX
Tip: 正確的做法是當我們不確定一個對象是否能接收某個消息時着憨,應(yīng)該先判斷是否能響應(yīng)該方法:
if([self respondsToSelector:@selector(method)]){
[self performSelector:@selector(method)];
}
當一個對象無法接收某個消息時墩衙,就會啟動 消息轉(zhuǎn)發(fā)(message forwarding)”
機制。
如下圖:
可以看到甲抖,當一個函數(shù)的實現(xiàn)找不到時漆改,OC提供了三種補救的方式:
調(diào)用
resolveInstanceMethod
或是resolveClassMethod
嘗試去 resolve 這個消息。如果 resolve 方法返回 NO准谚,則調(diào)用
forwardingTargetForSelector
允許你把這個消息轉(zhuǎn)發(fā)給另一個對象挫剑。如果沒有新的目標對象返回,則調(diào)用
methodSignatureForSelector
和forwardInvocation
靈活的將目標函數(shù)以其他形式執(zhí)行柱衔。-
如果都不中樊破,那就GG了,Runtime會調(diào)用
doesNotRecognizeSelector:
拋出異常唆铐。?
下面我們看一個具體實例是如何進行補救的:
- 動態(tài)方法解析哲戚,如果在自己定義的Obj類中,沒有實現(xiàn)foo方法或链,我們可以實現(xiàn)
resolveInstanceMethod
方法惫恼,使用class_addMethod
添加一個函數(shù)實現(xiàn),并返回YES澳盐,就能夠成功進行補救祈纯。
#pragma mark 1. **動態(tài)方法解析**
//1.首先,Runtime會調(diào)用 +resolveInstanceMethod: 或者 +resolveClassMethod:叼耙,讓你有機會提供一個函數(shù)實現(xiàn)腕窥。如果你添加了函數(shù)并返回 YES, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(foo)) {
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
// 參數(shù)說明: (IMP)fooMethod 表示的是fooMethod的地址指針; "v@:" 意思是筛婉,v代表無返回值void簇爆,如果是i則代表int;@代表 id sel; : 代表 SEL _cmd; “v@:@@” 意思是爽撒,兩個參數(shù)的沒有返回值入蛆。
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj,SEL _cmd){
NSLog(@"成功添加了foo");
}
- 如果
resolveInstanceMethod
方法返回 NO ,并且沒有調(diào)用class_addMethod
添加實現(xiàn)方法硕勿;那么運行時就會移到下一步:消息轉(zhuǎn)發(fā)(Message Forwarding):如果目標對象實現(xiàn)了-forwardingTargetForSelector:
哨毁,Runtime 這時就會調(diào)用這個方法,給你把這個消息轉(zhuǎn)發(fā)給其他對象的機會源武。此處可以叫做Fast forwarding扼褪,因為這一步不會創(chuàng)建任何新的對象想幻,所以相比Normal forwarding 會快一些,注意:此處調(diào)用的是其他對象的實例方法话浇,所以也必須實例化該對象脏毯。
-
如果
forwardingTargetForSelector
返回了nil或self, 就會繼續(xù) Normal Fowarding ,這是最后一次挽救機會了幔崖。相比上一步的fast forwarding食店,這里會創(chuàng)建一個 NSInvocation 對象。首先它會發(fā)送-methodSignatureForSelector:
消息獲得函數(shù)的參數(shù)和返回值類型岖瑰。如果該方法返回 nil 叛买,Runtime 則會發(fā)出-doesNotRecognizeSelector:
消息,程序這時也就掛掉了蹋订;如果返回了一個函數(shù)簽名,Runtime 就會創(chuàng)建一個 NSInvocation 對象并發(fā)送-forwardInvocation:
消息給目標對象刻伊。NSInvocation 實際上就是對一個消息的描述露戒,包括selector 以及參數(shù)等信息。所以你可以在
-forwardInvocation:
里修改傳進來的 NSInvocation 對象捶箱,然后發(fā)送 -invokeWithTarget: 消息給它智什,傳進去一個新的目標。所以我們需要重寫這兩個方法:
methodSignatureForSelector:
和forwardInvocation:
丁屎。
至此荠锭,Runtime的調(diào)用流程就結(jié)束了。
?
四. Method Swizzling
我們知道晨川,每個類都有一個方法列表证九,存放著selector的名字和方法實現(xiàn)的映射關(guān)系。IMP類似于函數(shù)指針共虑,指向具體的Method實現(xiàn)愧怜。每一個SEL與一個IMP一一對應(yīng),正常情況下通過SEL可以查找到對應(yīng)消息的IMP實現(xiàn)妈拌。而Method Swizzling就可以將對應(yīng)的關(guān)系解開并映射到我們自定義的函數(shù)IMP上拥坛, KVO其實就是Apple使用了一個中間類,并進行了Swizzling尘分。Method Swizzling的好處就在于:不需要改動對應(yīng)類的源代碼猜惋,就可以更改某個方法的實現(xiàn)。
比如培愁,我們經(jīng)持ぃ可以在第三方框架中看到如下代碼,就是一段Method Swizzling:
//在整個文件被加載到運行時,在 main 函數(shù)調(diào)用之前被 ObjC 運行時調(diào)用的鉤子方法
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(XXX_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
//主類本身沒有實現(xiàn)需要替換的方法竭钝,而是繼承了父類的實現(xiàn)梨撞,即 class_addMethod 方法返回 YES 雹洗。這時使用 class_getInstanceMethod 函數(shù)獲取到的 originalSelector 指向的就是父類的方法,我們再通過執(zhí)行 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 將父類的實現(xiàn)替換到我們自定義的 XXX_viewWillAppear 方法中卧波。這樣就達到了在 XXX_viewWillAppear 方法的實現(xiàn)中調(diào)用父類實現(xiàn)的目的时肿。
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
//主類本身有實現(xiàn)需要替換的方法,也就是 class_addMethod 方法返回 NO 港粱。這種情況的處理比較簡單螃成,直接交換兩個方法的實現(xiàn)就可以了
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)XXX_viewWillAppear:(BOOL)animated
{
// Method Swizzling之后, 調(diào)用XXX_viewWillAppear:實際執(zhí)行的代碼已經(jīng)是原來viewWillAppear中的代碼了
[self XXX_viewWillAppear:animated];
// 添加某些操作
}
參考文獻: