Objective-c Runtime專題總結
原文http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
1OC與Runtime的交互方式
OC從三種不同的層級上與Runtime系統(tǒng)進行交互,分別是通過Objective-C源代碼,通過Foundation框架的NSObject類定義的方法,通過對runtime函數的直接調用。
1.1Objective-C源代碼
大部分情況下你就只管寫你的OC代碼就行,runtime系統(tǒng)自動在幕后辛勤勞作著。
1.2NSObject的方法
Cocoa中大多數類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它十個抽象超類,它實現了一些消息轉發(fā)有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它并為你定義的類提供描述內容。NSObject還有些方法能在運行時獲得類的信息,并檢查一些特性,比如class返回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應指定的消息;conformsToProtocol:檢查對象是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。
1.3Runtime的函數
Runtime系統(tǒng)是一個由一系列函數和數據結構組成,具有公共接口的動態(tài)共享庫。頭文件存放于/usr/include/objc目錄下。許多函數允許你用純C代碼來重復實現OC中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫OC代碼時一般不會直接用到這些函數的,除非是寫一些OC與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對Runtime函數的詳細文檔。
2Runtime術語
id objc_msgSend( id self, SEL op, ... );
2.1SEL
objc_msgSend函數第二個參數類型為SEL,它是selector在OC中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區(qū)分方法的ID,而這個ID的數據結構是SEL :
typedef structobjc_selector *SEL;
其實它就是個映射到方法的C字符串,你可以用OC編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數來獲得一個SEL類型的方法選擇器。
不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器,于是OC中方法命名有時會帶上參數類型( NSNumber一堆抽象工廠方法拿走不謝),Cocoa中有好多長長的方法哦。
2.2Id與objc_object結構體
objc_msgSend第一個參數類型為id,大家對它都不陌生,它是一個指向類實例的指針:
typedefstructobjc_object*id;
那objc_object又是啥呢:
structobjc_object{Classisa;};
objc_object結構體包含一個isa指針,根據isa指針就可以順藤摸瓜找到對象所屬的類。
2.3Class
之所以說isa是指針是因為Class其實是一個指向objc_class結構體的指針:
typedefstructobjc_class*Class;
而objc_class就是我們摸到的那個瓜,里面的東西多著呢:
structobjc_class{
ClassisaOBJC_ISA_AVAILABILITY;
#if!__OBJC2__
Classsuper_classOBJC2_UNAVAILABLE;
constchar*nameOBJC2_UNAVAILABLE;
longversionOBJC2_UNAVAILABLE;
longinfoOBJC2_UNAVAILABLE;
longinstance_sizeOBJC2_UNAVAILABLE;
structobjc_ivar_list*ivarsOBJC2_UNAVAILABLE;
structobjc_method_list**methodListsOBJC2_UNAVAILABLE;
structobjc_cache*cacheOBJC2_UNAVAILABLE;
structobjc_protocol_list*protocolsOBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;
可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。
其中objc_ivar_list和objc_method_list分別是成員變量列表和方法列表:
structobjc_ivar_list{
intivar_countOBJC2_UNAVAILABLE;#ifdef __LP64__intspaceOBJC2_UNAVAILABLE;
#endif/* variable length structure */
structobjc_ivarivar_list[1]OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;
structobjc_method_list{
structobjc_method_list*obsoleteOBJC2_UNAVAILABLE;
intmethod_countOBJC2_UNAVAILABLE;
#ifdef __LP64__
intspaceOBJC2_UNAVAILABLE;
#endif
/* variable length structure */
structobjc_methodmethod_list[1]OBJC2_UNAVAILABLE;
}
2.4元類(Meta Class)
一個ObjC類同時也是一個對象,為了處理類和對象的關系,runtime庫創(chuàng)建了一種叫做元類(Meta Class)的東西。當你發(fā)出一個類似[NSObject alloc]的消息時,你事實上是把這個消息發(fā)給了一個類對象(Class Object),這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類(root meta class)的實例。你會說NSObject的子類時,你的類就會指向NSObject做為其超類。但是所有的元類都指向根元類為其超類。所有的元類的方法列表都有能夠響應消息的類方法。所以當[NSObject alloc]這條消息發(fā)給類對象的時候,objc_msgSend()會去它的元類里面去查找能夠響應消息的方法,如果找到了,然后對這個類對象執(zhí)行方法調用。
上圖實線是super_class指針,虛線是isa指針。有趣的是根元類的超類是NSObject,而isa指向了自己,而NSObject的超類為nil,也就是它沒有超類。
2.5Method
Method是一種代表類中的某個方法的類型。
typedefstructobjc_method*Method;
而objc_method在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:
structobjc_method{
SELmethod_nameOBJC2_UNAVAILABLE;
char*method_typesOBJC2_UNAVAILABLE;
IMPmethod_impOBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;
·方法名類型為SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
·方法類型method_types是個char指針,其實存儲著方法的參數類型和返回值類型。
·method_imp指向了方法的實現,本質上是一個函數指針,后面會詳細講到。
2.6Ivar
Ivar是一種代表類中實例變量的類型。
typedefstructobjc_ivar*Ivar;
而objc_ivar在上面的成員變量列表中也提到過:
tructobjc_ivar{
char*ivar_nameOBJC2_UNAVAILABLE;
char*ivar_typeOBJC2_UNAVAILABLE;
intivar_offsetOBJC2_UNAVAILABLE;
#ifdef __LP64__
intspaceOBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;
PS:OBJC2_UNAVAILABLE之類的宏定義是蘋果在OC中對系統(tǒng)運行版本進行約束的黑魔法,有興趣的可以查看源代碼。
2.7IMP函數指針
IMP在objc.h中的定義是:
typedefid(*IMP)(id,SEL,...);
它就是一個函數指針,這是由編譯器生成的。當你發(fā)起一個ObjC消息之后,最終它會執(zhí)行的那段代碼,就是由這個函數指針指定的。而IMP這個函數指針就指向了這個方法的實現。既然得到了執(zhí)行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面會提到。
你會發(fā)現IMP指向的方法與objc_msgSend函數類型相同,參數都包含id和SEL類型。每個方法名都對應一個SEL類型的方法選擇器,而每個實例對象中的SEL對應的方法實現肯定是唯一的,通過一組id和SEL參數就能確定唯一的方法實現地址;反之亦然。
2.8Cache
在runtime.h中Cache的定義如下:
typedefstructobjc_cache*Cache
還記得之前objc_class結構體中有一個struct
objc_cache *cache吧,它到底是緩存啥的呢,先看看objc_cache的實現:
structobjc_cache{
unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;
unsignedintoccupiedOBJC2_UNAVAILABLE;
Methodbuckets[1]OBJC2_UNAVAILABLE;
};
Cache為方法調用的性能進行優(yōu)化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優(yōu)先在Cache中查找。Runtime系統(tǒng)會把被調用的方法存到Cache中(理論上講一個方法如果被調用,那么它有可能今后還會被調用),下次查找的時候效率更高。
3消息
OC中發(fā)送消息是用中括號([])把接收者和消息括起來,而直到運行時才會把消息與方法實現綁定。
3.1objc_msgSend函數
看起來像是objc_msgSend返回了數據,其實objc_msgSend從不返回數據而是你的方法被調用后返回了數據。下面詳細敘述下消息發(fā)送步驟:
1.檢測這個selector是不是要忽略的。比如Mac OS X開發(fā),有了垃圾回收就不理會retain,release這些函數了。
2.檢測這個target是不是nil對象。ObjC的特性是允許對一個nil對象執(zhí)行任何一個方法不會Crash,因為會被忽略掉。
3.如果上面兩個都過了,那就開始查找這個類的IMP,先從cache里面找,完了找得到就跳到對應的函數去執(zhí)行。
4.如果cache找不到就找一下方法分發(fā)表。(Class中的方法列表)
5.如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
6.如果還找不到就要開始進入動態(tài)方法解析了,后面會提到。
其實編譯器會根據情況在objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,或objc_msgSendSuper_stret四個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用名字帶有”Super”的函數;如果消息返回值是數據結構而不是簡單值時,那么會調用名字帶有”stret”的函數。排列組合正好四個方法。
3.2方法中的隱藏參數
我們經常在方法中使用self關鍵字來引用實例本身,但從沒有想過為什么self就能取到調用當前方法的對象吧。其實self的內容是在方法運行時被偷偷地動態(tài)傳入的。
當objc_msgSend找到方法對應的實現時,它將直接調用該方法實現,并將消息中所有的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:
–接收消息的對象(也就是self指向的內容)
–方法選擇器(_cmd指向的內容)
之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數。它們是在代碼被編譯時被插入實現中的。盡管這些參數沒有被明確聲明,在源代碼中我們仍然可以引用它們。在下面的例子中,self引用了接收者對象,而_cmd引用了方法本身的選擇器:
- strange {
idtarget = getTheReceiver();
SEL method = getTheMethod();
if( target ==self|| method == _cmd )returnnil;
return[targetperformSelector:method];
}
在這兩個參數中,self更有用。實際上,它是在方法實現中訪問消息接收者對象的實例變量的途徑。
而當方法中的super關鍵字接收到消息時,編譯器會創(chuàng)建一個objc_super結構體:
struct objc_super { id receiver;Classclass; };
這個結構體指明了消息應該被傳遞給特定超類的定義。
3.3獲取方法地址
在IMP那節(jié)提到過可以避開消息綁定而直接獲取方法的地址并調用方法。這種做法很少用,除非是需要持續(xù)大量重復調用某方法的極端情況,避開消息發(fā)送泛濫而直接調用該方法會更高效。
NSObject類中有個methodForSelector:實例方法,你可以用它來獲取某個方法選擇器對應的IMP,舉個栗子:
void(*setter)(id, SEL,? BOOL);
inti;
setter = (void(*)(id, SEL,? BOOL))[target methodForSelector:@selector(setFilled:)];
for( i =0; i <1000; i++ )
setter(targetList[i], @selector(setFilled:), YES);
PS:methodForSelector:方法是由Cocoa的Runtime系統(tǒng)提供的,而不是OC自身的特性。
4動態(tài)方法解析
你可以動態(tài)地提供一個方法的實現。例如我們可以用@dynamic關鍵字在類的實現文件中修飾一個屬性:
@dynamicpropertyName;
這表明我們會為這個屬性動態(tài)提供存取方法,也就是說編譯器不會再默認為我們生成setPropertyName:和propertyName方法,而需要我們動態(tài)提供。我們可以通過分別重載resolveInstanceMethod:和resolveClassMethod:方法分別添加實例方法實現和類方法實現。因為當Runtime系統(tǒng)在Cache和方法分發(fā)表中(包括超類)找不到要執(zhí)行的方法時,Runtime會調用resolveInstanceMethod:或resolveClassMethod:來給程序員一次動態(tài)添加方法實現的機會。我們需要用class_addMethod函數完成向特定類添加特定方法實現的操作:
void dynamicMethodIMP(idself,? SEL _cmd) {
//implementation ....
}
@implementationMyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if(aSEL ==@selector(resolveThisMethodDynamically))? {
class_addMethod([selfclass],? aSEL, (IMP) dynamicMethodIMP,"v@:");
returnYES;
}
return[superresolveInstanceMethod:aSEL];
}
@end
上面的例子為resolveThisMethodDynamically方法添加了實現內容,也就是dynamicMethodIMP方法中的代碼。其中“v@:”表示返回值和參數,這個符號涉及Type Encoding
PS:動態(tài)方法解析會在消息轉發(fā)機制浸入前執(zhí)行。如果respondsToSelector:或instancesRespondToSelector:方法被執(zhí)行,動態(tài)方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你想讓該方法選擇器被傳送到轉發(fā)機制,那么就讓resolveInstanceMethod:返回NO。
5消息轉發(fā)
5.1重定向
在消息轉發(fā)機制執(zhí)行前,Runtime系統(tǒng)會再給我們一次偷梁換柱的機會,即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者為其他對象:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector ==@selector(mysteriousMethod:)){
returnalternateObject;
}
return[superforwardingTargetForSelector:aSelector];
}
畢竟消息轉發(fā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回self,因為那樣會死循環(huán)。
5.2轉發(fā)
當動態(tài)方法解析不作處理返回NO時,消息轉發(fā)機制會被觸發(fā),這時forwardInvocation:方法會被執(zhí)行,我們可以重載這個方法來定義我們的轉發(fā)邏輯:
- (void)forwardInvocation:(NSInvocation? *)anInvocation {
if([someOtherObject? respondsToSelector: [anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else[superforwardInvocation:anInvocation];
}
該消息的唯一參數是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數。我們可以實現forwardInvocation:方法來對不能處理的消息做一些默認的處理,也可以將消息轉發(fā)給其他對象來處理,而不拋出錯誤。
當一個對象由于沒有相應的方法實現而無法響應某消息時,運行時系統(tǒng)將通過forwardInvocation:消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現只是簡單地調用了doesNotRecognizeSelector:。通過實現我們自己的forwardInvocation:方法,我們可以在該方法實現中將消息轉發(fā)給其它對象。
forwardInvocation:方法就像一個不能識別的消息的分發(fā)中心,將這些消息轉發(fā)給不同接收對象。或者它也可以象一個運輸站將所有的消息都發(fā)送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應,這一切都取決于方法的具體實現。該方法所提供是將不同的對象鏈接到消息鏈的能力。
注意:forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調用。所以,如果我們希望一個對象將negotiate消息轉發(fā)給其它對象,則這個對象不能有negotiate方法。否則,forwardInvocation:將不可能會被調用。
5.3轉發(fā)和多繼承
轉發(fā)和繼承相似,可以用于為OC編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發(fā)出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。
這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中Warrior和Diplomat沒有繼承關系,但是Warrior將negotiate消息轉發(fā)給了Diplomat后,就好似Diplomat是Warrior的超類一樣。
消息轉發(fā)彌補了OC不支持多繼承的性質,也避免了因為多繼承導致單個類變得臃腫復雜。它將問題分解得很細,只針對想要借鑒的方法才轉發(fā),而且轉發(fā)機制是透明的。
5.4替代者對象(Surrogate Objects)
轉發(fā)不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背后是強大的男人,畢竟女人遇到難題都把它們轉發(fā)給男人來做了。這里有一些適用案例,可以參看官方文檔。
5.5轉發(fā)與繼承
盡管轉發(fā)很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector:和isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發(fā)鏈。比如上圖中一個Warrior對象如果被問到是否能響應negotiate消息:
if([aWarriorrespondsToSelector:@selector(negotiate)])...
結果是NO,盡管它能夠接受negotiate消息而不報錯,因為它靠轉發(fā)消息給Diplomat類來響應消息。
如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior繼承到了Diplomat的negotiate方法,你得重新實現respondsToSelector:和isKindOfClass:來加入你的轉發(fā)算法:
-(BOOL)respondsToSelector:(SEL)aSelector{if([superrespondsToSelector:aSelector])returnYES;else{/* Here, test whether the aSelector message can** be forwarded to another object and whether that** object can respond to it. Return YES if it can.*/}returnNO;}
除了respondsToSelector:和isKindOfClass:之外,instancesRespondToSelector:中也應該寫一份轉發(fā)算法。如果使用了協議,conformsToProtocol:同樣也要加入到這一行列中。類似地,如果一個對象轉發(fā)它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發(fā)的消息。比如一個對象能給它的替代者對象轉發(fā)消息,它需要像下面這樣實現methodSignatureForSelector::
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector{
NSMethodSignature*signature=[supermethodSignatureForSelector:selector];
if(!signature){
signature=[surrogatemethodSignatureForSelector:selector];
}
returnsignature;
}
6壯的實例變量(NonFragile ivars)
在Runtime的現行版本中,最大的特點就是健壯的實例變量。當一個類被編譯時,實例變量的布局也就形成了,它表明訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據自己所占空間而產生位移:
上圖左邊是NSObject類的實例變量布局,右邊是我們寫的類的布局,也就是在超類后面加上我們自己類的實例變量,看起來不錯。但試想如果那天蘋果更新了NSObject類,發(fā)布新版本的系統(tǒng)的話,那就悲劇了:
我們自定義的類被劃了兩道線,那是因為那塊區(qū)域跟超類重疊了。唯有蘋果將超類改為以前的布局才能拯救我們,但這樣也導致它們不能再拓展它們的框架了,因為成員變量布局被死死地固定了。在脆弱的實例變量(Fragile
ivars)環(huán)境下我們需要重新編譯繼承自Apple的類來恢復兼容性。那么在健壯的實例變量下回發(fā)生什么呢?
在健壯的實例變量下編譯器生成的實例變量布局跟以前一樣,但是當runtime系統(tǒng)檢測到與超類有部分重疊時它會調整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。
需要注意的是在健壯的實例變量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass
class])代替;也不要使用offsetof(SomeClass,
SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass
class], "SomeIvar"))來代替。
7Objective-C Associated Objects
在OS X 10.6之后,Runtime系統(tǒng)讓OC支持向對象動態(tài)添加變量。涉及到的函數有以下三個:
voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue,objc_AssociationPolicypolicy);
idobjc_getAssociatedObject(idobject,constvoid*key);
voidobjc_removeAssociatedObjects(idobject);
這些方法以鍵值對的形式動態(tài)地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:
enum{
OBJC_ASSOCIATION_ASSIGN=0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC=1,
OBJC_ASSOCIATION_COPY_NONATOMIC=3,
OBJC_ASSOCIATION_RETAIN=01401,
OBJC_ASSOCIATION_COPY=01403
};
這些常量對應著引用關聯值的政策,也就是OC內存管理的引用計數機制。
8總結
我們之所以讓自己的類繼承NSObject不僅僅因為蘋果幫我們完成了復雜的內存分配問題,更是因為這使得我們能夠用上Runtime系統(tǒng)帶來的便利。可能我們平時寫代碼時可能很少會考慮一句簡單的[receiver
message]背后發(fā)生了什么,而只是當做方法或函數調用。深入理解Runtime系統(tǒng)的細節(jié)更有利于我們利用消息機制寫出功能更強大的代碼,比如Method Swizzling等。
9參考鏈接
–Objective-C Runtime Programming Guide
–Understanding the Objective-C Runtime
Objective-C Runtime Programming Guide
深入理解Objective-C的Runtime機制
http://www.csdn.net/article/2015-07-06/2825133-objective-c-runtime/1
Objective-C Runtime運行時之一:類與對象
http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/
Objective-C Runtime運行時之二:成員變量與屬性
Objective-C Runtime運行時之三:方法與消息
Objective-C Runtime運行時之四:Method Swizzling
Objective-C Runtime運行時之五:協議與分類
Objective-C Runtime運行時之六:拾遺
http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/