Runtime是深入學習OC的必經(jīng)之路吉嚣,所以花了一些時間研究了一下梢薪。
一、介紹RunTime(運行時):
OC 語言是一門動態(tài)語言尝哆,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了運行時來處理秉撇。
對于OC來說,這個運行時系統(tǒng)就像一個操作系統(tǒng)一樣:它讓所有的工作可以正常的運行秋泄。Runtime基本上是用C和匯編寫的琐馆,這個庫使得C語言有了面向?qū)ο蟮哪芰Α?/p>
在Runtime中,對象可以用C語言中的結(jié)構(gòu)體表示恒序,而方法可以用C函數(shù)來實現(xiàn)瘦麸,另外再加上了一些額外的特性。這些結(jié)構(gòu)體和函數(shù)被runtime函數(shù)封裝后歧胁,讓OC的面向?qū)ο缶幊套優(yōu)榭赡堋?/p>
找出方法的最終執(zhí)行代碼:當程序執(zhí)行[object doSomething]時滋饲,會向消息接收者(object)發(fā)送一條消息(doSomething),runtime會根據(jù)消息接收者是否能響應(yīng)該消息而做出不同的反應(yīng)喊巍。
二屠缭、類與對象的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
OC類是由Class類型來表示的,它實際上是一個指
向objc_class結(jié)構(gòu)體的指針崭参。
typedef struct object_class *Class
其定義如下:
查看objc/runtime.h中objc_class結(jié)構(gòu)體的定義如下:
struct object_class{? ?
Class isa OBJC_ISA_AVAILABILITY;
#if!__OBJC2__
Class super_class? ? ? ? ? ? ? ? ? ? ? ?
OBJC2_UNAVAILABLE;// 父類constchar*name? ? ? ? ? ? ? ?
OBJC2_UNAVAILABLE;// 類名longversion
OBJC2_UNAVAILABLE;// 類的版本信息呵曹,默認為0longinfo? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;// 類信息,供運行期使用的一些位標識longinstance_size? ? ? ? ? ? ? ? ?? OBJC2_UNAVAILABLE;// 該類的實例變量大小structobjc_ivar_list *ivars? ? ? ? ? ? OBJC2_UNAVAILABLE;// 該類的成員變量鏈表structobjc_method_list *methodLists? ? OBJC2_UNAVAILABLE;// 方法定義的鏈表structobjc_cache *cache? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;// 方法緩存structobjc_protocol_list *protocols? ? OBJC2_UNAVAILABLE;// 協(xié)議鏈表#endif}OBJC2_UNAVAILABLE;
其執(zhí)行過程:
NSArray *array = [[NSArray alloc] init];
objc_object
objc_object是表示一個類的實例的結(jié)構(gòu)體
它的定義如下(objc/objc.h):
structobjc_object{? ? Class isa OBJC_ISA_AVAILABILITY;};typedefstructobjc_object*id;
可以看到,這個結(jié)構(gòu)體只有一個字體奄喂,即指向其類的isa指針之剧。這
樣,當我們向一個Objective-C對象發(fā)送消息時砍聊,運行時庫會根據(jù)
實例對象的isa指針找到這個實例對象所屬的類背稼。Runtime庫會在類
的方法列表及父類的方法列表中去尋找與消息對應(yīng)的selector指向
的方法,找到后即運行這個方法玻蝌。
元類(Meta Class)
meta-class是一個類對象的類蟹肘。
在上面我們提到,所有的類自身也是一個對象俯树,我們可以向這個對象發(fā)送消息(即調(diào)用類方法)帘腹。
既然是對象,那么它也是一個objc_object指針许饿,它包含一個指向其類的一個isa指針阳欲。那么,這個isa指針指向什么呢陋率?
為了調(diào)用類方法球化,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結(jié)構(gòu)體。這就引出了meta-class的概念瓦糟,meta-class中存儲著一個類的所有類方法筒愚。
所以,調(diào)用類方法的這個類對象的isa指針指向的就是meta-class
當我們向一個對象發(fā)送消息時菩浙,runtime會在這個對象所屬的這個類的方法列表中查找方法巢掺;而向一個類發(fā)送消息時,會在這個類的meta-class的方法列表中查找劲蜻。
再深入一下陆淀,meta-class也是一個類,也可以向它發(fā)送一個消息先嬉,那么它的isa又是指向什么呢轧苫?為了不讓這種結(jié)構(gòu)無限延伸下去,Objective-C的設(shè)計者讓所有的meta-class的isa指向基類的meta-class坝初,以此作為它們的所屬類浸剩。
即钾军,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類鳄袍,而基類的meta-class的isa指針是指向它自己。
通過上面的描述吏恭,再加上對objc_class結(jié)構(gòu)體中super_class指針的分析拗小,我們就可以描繪出類及相應(yīng)meta-class類的一個繼承體系了,如下代碼
Snip20160501_1.png
Category
Category是表示一個指向分類的結(jié)構(gòu)體的指針樱哼,其定義如下:
typedefstructobjc_category*Categorystructobjc_category{char*category_name? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;// 分類名char*class_name? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;// 分類所屬的類名structobjc_method_list*instance_methods? OBJC2_UNAVAILABLE;// 實例方法列表structobjc_method_list*class_methods? ? ? OBJC2_UNAVAILABLE;// 類方法列表structobjc_protocol_list*protocols? ? ? ? OBJC2_UNAVAILABLE;// 分類所實現(xiàn)的協(xié)議列表}
這個結(jié)構(gòu)體主要包含了分類定義的實例方法與類方法哀九,其中instance_methods列表是objc_class中方法列表的一個子集剿配,而class_methods列表是元類方法列表的一個子集。
可發(fā)現(xiàn)阅束,類別中沒有ivar成員變量指針呼胚,也就意味著:類別中不能夠添加實例變量和屬性
structobjc_ivar_list*ivars? ? ? ? ? ? OBJC2_UNAVAILABLE;// 該類的成員變量鏈表
三、runtime關(guān)聯(lián)對象
我們先看看關(guān)聯(lián)API息裸,只有這三個API蝇更,使用也是非常簡單的:
1.設(shè)置關(guān)聯(lián)值
參數(shù)說明:
object:與誰關(guān)聯(lián),通常是傳self
key:唯一鍵呼盆,在獲取值時通過該鍵獲取年扩,通常是使用static
const void *來聲明
value:關(guān)聯(lián)所設(shè)置的值
policy:內(nèi)存管理策略,比如使用copy
voidobjc_setAssociatedObject(idobject,constvoid*key, idvalue, objc _AssociationPolicy policy)
2.獲取關(guān)聯(lián)值
參數(shù)說明:
object:與誰關(guān)聯(lián)访圃,通常是傳self厨幻,在設(shè)置關(guān)聯(lián)時所指定的與哪個對象關(guān)聯(lián)的那個對象
key:唯一鍵,在設(shè)置關(guān)聯(lián)時所指定的鍵
idobjc_getAssociatedObject(idobject,constvoid*key)
3.取消關(guān)聯(lián)
voidobjc_removeAssociatedObjects(idobject)
關(guān)聯(lián)策略
使用場景:
可以在類別中添加屬性
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){OBJC_ASSOCIATION_ASSIGN =0,// 表示弱引用關(guān)聯(lián)腿时,通常是基本數(shù)據(jù)類型OBJC_ASSOCIATION_RETAIN_NONATOMIC =1,// 表示強引用關(guān)聯(lián)對象况脆,是線程安全的OBJC_ASSOCIATION_COPY_NONATOMIC =3,// 表示關(guān)聯(lián)對象copy,是線程安全的OBJC_ASSOCIATION_RETAIN =01401,// 表示強引用關(guān)聯(lián)對象批糟,不是線程安全的OBJC_ASSOCIATION_COPY =01403// 表示關(guān)聯(lián)對象copy漠另,不是線程安全的};
四、方法與消息
1跃赚、SEL
SEL又叫選擇器笆搓,是表示一個方法的selector的指針,其定義如下:
typedefstructobjc_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贺待、IMP
IMP實際上是一個函數(shù)指針,指向方法實現(xiàn)的地址零截。
其定義如下:
id (*IMP)(id, SEL,...)
第一個參數(shù):是指向self的指針(如果是實例方法麸塞,則是類實例的內(nèi)存地址;如果是類方法涧衙,則是指向元類的指針)
第二個參數(shù):是方法選擇器(selector)
接下來的參數(shù):方法的參數(shù)列表哪工。
前面介紹過的SEL就是為了查找方法的最終實現(xiàn)IMP的。由于每個方法對應(yīng)唯一的SEL弧哎,因此我們可以通過SEL方便快速準確地獲得它所對應(yīng)的IMP雁比,查找過程將在下面討論。取得IMP后撤嫩,我們就獲得了執(zhí)行這個方法代碼的入口點偎捎,此時,我們就可以像調(diào)用普通的C語言函數(shù)一樣來使用這個函數(shù)指針了序攘。
3茴她、Method
Method用于表示類定義中的方法,則定義如下:
typedef struct objc_method *Methodstructobjc_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)代碼赡麦。
4朴皆、方法調(diào)用流程
Snip20160501_2.png
在Objective-C中帕识,消息直到運行時才綁定到方法實現(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,...)
這個函數(shù)完成了動態(tài)綁定的所有事情:
a伪货、首先它找到selector對應(yīng)的方法實現(xiàn)们衙。因為同一個方法可
能在不同的類中有不同的實現(xiàn),所以我們需要依賴于接收者的類
來找到的確切的實現(xiàn)碱呼。
b蒙挑、調(diào)用方法實現(xiàn),并將接收者對象及方法的所有參數(shù)傳給它愚臀。
c忆蚀、最后,它將實現(xiàn)返回的值作為它自己的返回值姑裂。
消息的關(guān)鍵在于我們前面章節(jié)討論過的結(jié)構(gòu)體objc_class馋袜,這個結(jié)構(gòu)體有兩個字段是我們在分發(fā)消息的關(guān)注的:
-> 指向父類的指針
-> 個類的方法分發(fā)表,即methodLists舶斧。
當我們創(chuàng)建一個新對象時欣鳖,先為其分配內(nèi)存,并初始化其成員變量茴厉。其中isa指針也會被初始化泽台,讓對象可以訪問類及類的繼承體系。
下圖演示了這樣一個消息的基本框架:
當消息發(fā)送給一個對象時首先從運行時系統(tǒng)緩存使用過的方法中尋找矾缓。
如果找到师痕,執(zhí)行該方法,如未找到繼續(xù)執(zhí)行下面的步驟
objc_msgSend通過對象的isa指針獲取到類的結(jié)構(gòu)體,然后在方法分發(fā)表里面查找方法的selector而账。
如果沒有找到selector胰坟,objc_msgSend結(jié)構(gòu)體中的指向父類的指針找到其父類,并在父類的分發(fā)表里面查找方法的selector泞辐。
依此笔横,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector咐吼,函數(shù)會就獲取到了實現(xiàn)的入口點吹缔,并傳入相應(yīng)的參數(shù)來執(zhí)行方法的具體實現(xiàn),并將該方法添加進入緩存中如果最后沒有定位到selector,則會走消息轉(zhuǎn)發(fā)流程锯茄,這個我們在后面討論厢塘。
5茶没、消息轉(zhuǎn)發(fā)
當一個對象能接收一個消息時,就會走正常的方法調(diào)用流程晚碾。但如果一個對象無法接收指定消息時抓半,又會發(fā)生什么事呢?默認情況下格嘁,如果是以[object message]的方式調(diào)用方法笛求,如果object無法響應(yīng)message消息時,編譯器會報錯糕簿。但如果是以perform…的形式來調(diào)用探入,則需要等到運行時才能確定object是否能接收message消息。如果不能懂诗,則程序崩潰蜂嗽。
Snip20160501_3.png
通常,當我們不能確定一個對象是否能接收某個消息時殃恒,會先調(diào)用respondsToSelector:來判斷一下植旧。如下代碼所示:
if([selfrespondsToSelector:@selector(method)]){[self performSelector:@selector(method)];}
不過,我們這邊想討論下不使用respondsToSelector:判斷的情況芋类。這才是我們這一節(jié)的重點隆嗅。
當一個對象無法接收某一消息時,就會啟動所謂“消息轉(zhuǎn)發(fā)(message forwarding)”機制侯繁,通過這一機制胖喳,我們可以告訴對象如何處理未知的消息。默認情況下贮竟,對象接收到未知的消息丽焊,會導致程序崩潰,通過控制臺咕别,我們可以看到以下異常信息:
這段異常信息實際上是由NSObject的“doesNotRecognizeSelector”方法拋出的技健。不過,我們可以采取一些措施惰拱,讓我們的程序執(zhí)行特定的邏輯雌贱,而避免程序的崩潰。
消息轉(zhuǎn)發(fā)機制基本上分為三個步驟:
1>偿短、動態(tài)方法解析
2>欣孤、備用接收者
3>、完整轉(zhuǎn)發(fā)
消息的轉(zhuǎn)發(fā)流程圖:
Snip20160501_5.png
動態(tài)方法解析
對象在接收到未知的消息時昔逗,首先會調(diào)用所屬類的類方法
+resolveInstanceMethod:(實例方法)或者
+resolveClassMethod:(類方法)降传。
在這個方法中,我們有機會為該未知消息新增一個“處理方法”勾怒,通過運行時class_addMethod函數(shù)動態(tài)添加到類里面就可以了婆排。
這種方案更多的是為了實現(xiàn)@dynamic屬性声旺。
備用接收者
-(id)forwardingTargetForSelector:(SEL)aSelector
如果在上一步無法處理消息,則Runtime會繼續(xù)調(diào)以下方法:
如果一個對象實現(xiàn)了這個方法段只,并返回一個非nil的結(jié)果腮猖,則這個對象會作為消息的新接收者,且消息會被分發(fā)到這個對象翼悴。當然這個對象不能是self自身缚够,否則就是出現(xiàn)無限循環(huán)幔妨。當然鹦赎,如果我們沒有指定相應(yīng)的對象來處理aSelector,則應(yīng)該調(diào)用父類的實現(xiàn)來返回結(jié)果误堡。
這一步合適于我們只想將消息轉(zhuǎn)發(fā)到另一個能處理該消息的對象上古话。但這一步無法對消息進行處理,如操作消息的參數(shù)和返回值锁施。
完整消息轉(zhuǎn)發(fā)
如果在上一步還不能處理未知消息陪踩,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機制了。
我們首先要通過,指定方法簽名悉抵,若返回nil肩狂,則表示不處理。
如下代碼:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{if([NSStringFromSelector(aSelector)isEqualToString:@"testInstanceMethod"]){return[NSMethodSignaturesignatureWithObjcTypes:"v@:"];? }return[supermethodSignatureForSelector:aSelector];}
若返回方法簽名姥饰,則會進入下一步調(diào)用以下方法傻谁,對象會創(chuàng)建一個表示消息的NSInvocation對象,把與尚未處理的消息有關(guān)的全部細節(jié)都封裝在anInvocation中列粪,包括selector审磁,目標(target)和參數(shù)。
我們可以在forwardInvocation方法中選擇將消息轉(zhuǎn)發(fā)給其它對象岂座。我們可以通過anInvocation對象做很多處理态蒂,比如修改實現(xiàn)方法,修改響應(yīng)對象等.
如下所示:
- (void)forwardInvovation:(NSInvocation)anInvocation{? ? [anInvocationinvokeWithTarget:_helper];? ? [anInvocationsetSelector:@selector(run)];? ? [anInvocationinvokeWithTarget:self];}
五费什、Method Swizzling
1.Swizzling原理
在Objective-C中調(diào)用一個方法钾恢,其實是向一個對象發(fā)送消息,而查找消息的唯一依據(jù)是selector的名字鸳址。所以瘩蚪,我們可以利用Objective-C的runtime機制,實現(xiàn)在運行時交換selector對應(yīng)的方法實現(xiàn)以達到我們的目的氯质。
每個類都有一個方法列表募舟,存放著selector的名字和方法實現(xiàn)的映射關(guān)系。IMP有點類似函數(shù)指針闻察,指向具體的Method實現(xiàn)
我們先看看SEL與IMP之間的關(guān)系圖:
Snip20160501_6.png
從上圖可以看出來拱礁,每一個SEL與一個IMP一一對應(yīng)琢锋,正常情況下通過SEL可以查找到對應(yīng)消息的IMP實現(xiàn)。
但是呢灶,現(xiàn)在我們要做的就是把鏈接線解開吴超,然后連到我們自定義的函數(shù)的IMP上。當然鸯乃,交換了兩個SEL的IMP鲸阻,還是可以再次交換回來了。交換后變成這樣的缨睡,如下圖
Snip20160501_7.png
從圖中可以看出鸟悴,我們通過swizzling特性,將selectorC的方法實現(xiàn)IMPc與selectorN的方法實現(xiàn)IMPn交換了奖年,當我們調(diào)用selectorC细诸,也就是給對象發(fā)送selectorC消息時,所查找到的對應(yīng)的方法實現(xiàn)就是IMPn而不是IMPc了陋守。