Objective-C 的 Runtime

我們常常會聽說 Objective-C 是一門動態(tài)語言侣灶,那么這個「動態(tài)」表現(xiàn)在哪呢倔既?我想最主要的表現(xiàn)就是 Objective-C 把很多靜態(tài)語言在編譯和鏈接時(shí)做的事情放到了運(yùn)行時(shí)去處理,它在運(yùn)行時(shí)實(shí)現(xiàn)了對類切心、方法飒筑、成員變量、屬性等信息的管理機(jī)制绽昏,這一套運(yùn)行時(shí)機(jī)制為我們開發(fā)提供了極大的靈活性协屡,比如:

在運(yùn)行時(shí)創(chuàng)建或修改一個類。

在運(yùn)行時(shí)修改成員變量全谤、屬性肤晓。

在運(yùn)行時(shí)進(jìn)行消息分發(fā)和方法綁定。

與之對應(yīng)的實(shí)現(xiàn)就是 Objective-C 短小精悍的 Runtime认然。對于蘋果維護(hù)的 Objective-C 的 Runtime 源碼补憾,你可以在這里看到:Objective-C 源碼

運(yùn)行時(shí)的類與對象

相關(guān)函數(shù)

Objective-C 的 Runtime 為我們提供了很多運(yùn)行時(shí)狀態(tài)下跟類與對象相關(guān)的函數(shù)卷员,比如:

const char *class_getName(Class cls)余蟹,獲取指定類的類名。

BOOL class_isMetaClass(Class cls)子刮,判斷指定類是否是一個元類威酒。

Class class_getSuperclass(Class cls)窑睁,獲取指定類的父類。

Class class_setSuperclass(Class cls, Class newSuper)葵孤,設(shè)定指定類的父類担钮。

int class_getVersion(Class cls),獲取指定類的版本信息尤仍。

void class_setVersion(Class cls, int version)箫津,設(shè)定指定類的版本信息。

size_t class_getInstanceSize(Class cls)宰啦,獲取實(shí)例大小苏遥。

Ivar class_getInstanceVariable(Class cls, const char *name),獲取指定名字的實(shí)例變量赡模。

Ivar class_getClassVariable(Class cls, const char *name)田炭,獲取指定名字的類變量。

Ivar *class_copyIvarList(Class cls, unsigned int *outCount)漓柑,獲取類的成員變量列表的拷貝教硫。調(diào)用后需要自己 free()。

Method class_getInstanceMethod(Class cls, SEL name)辆布,獲取指定名字的實(shí)例方法瞬矩。

Method class_getClassMethod(Class cls, SEL name),獲取指定名字的類方法锋玲。

IMP class_getMethodImplementation(Class cls, SEL name)景用,獲取指定名字的方法實(shí)現(xiàn)。

BOOL class_respondsToSelector(Class cls, SEL sel)惭蹂,類是否響應(yīng)指定的方法伞插。

Method *class_copyMethodList(Class cls, unsigned int *outCount),獲取方法列表的拷貝剿干。調(diào)用后需要自己 free()。

BOOL class_conformsToProtocol(Class cls, Protocol *protocol)穆刻,類是否遵循指定的協(xié)議置尔。

Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount),獲取協(xié)議列表的拷貝氢伟。調(diào)用后需要自己 free()榜轿。

objc_property_t class_getProperty(Class cls, const char *name),獲取指定名字的屬性朵锣。

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)谬盐,獲取類的屬性列表。調(diào)用后需要自己 free()诚些。

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)飞傀,為類添加方法皇型。

IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types),替代類的方法砸烦。

BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)弃鸦,給指定的類添加成員變量。這個函數(shù)只能在 objc_allocateClassPair() 和 objc_registerClassPair() 之間調(diào)用幢痘,并且不能為一個已經(jīng)存在的類添加成員變量唬格。

BOOL class_addProtocol(Class cls, Protocol *protocol),為類添加協(xié)議颜说。

BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)购岗,為類添加屬性。

void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)门粪,替代類的屬性喊积。

id class_createInstance(Class cls, size_t extraBytes),創(chuàng)建指定類的實(shí)例庄拇。

id objc_constructInstance(Class cls, void *bytes)注服,在指定的位置創(chuàng)建類的實(shí)例。

void *objc_destructInstance(id obj)措近,銷毀實(shí)例溶弟。

Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes),創(chuàng)建類和元類瞭郑。

void objc_registerClassPair(Class cls),注冊類到 Runtime屈张。

void objc_disposeClassPair(Class cls)碳抄,銷毀類和對應(yīng)的元類璧尸。

上面羅列的函數(shù)只是一部分熬拒,在使用這些函數(shù)時(shí)爷光,有時(shí)候需要注意一些細(xì)節(jié)信息和使用規(guī)范,具體可以查閱Objective-C Runtime Reference澎粟。

類的數(shù)據(jù)結(jié)構(gòu)

上面的函數(shù)非常豐富蛀序,我們可以看出這些函數(shù)為我們提供了在運(yùn)行時(shí)改變一個類的結(jié)構(gòu)欢瞪、屬性、方法哼拔、協(xié)議等信息的能力引有。那這些函數(shù)背后所操作的數(shù)據(jù)結(jié)構(gòu)是什么樣的呢?這個其實(shí)可以在objc/runtime.h的源碼中查到:

structobjc_class {? ? Class isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__Class super_class OBJC2_UNAVAILABLE;// 父類倦逐。constchar*name OBJC2_UNAVAILABLE;// 類名譬正。longversion OBJC2_UNAVAILABLE;// 類的版本信息。longinfo OBJC2_UNAVAILABLE;// 類信息檬姥,供運(yùn)行時(shí)使用的一些位標(biāo)識曾我。longinstance_size OBJC2_UNAVAILABLE;// 類的實(shí)例變量大小。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;

這個結(jié)構(gòu)其實(shí)就是類的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)蛉谜,在 Objective-C 中類是由Class表示,而 Class 實(shí)際上是一個指向struct objc_class的指針崇堵。

typedefstructobjc_class *Class;

在這個類的數(shù)據(jù)結(jié)構(gòu)中逻炊,有幾個字段需要再解釋一下:

isa累魔,在大多面向?qū)ο蟮恼Z言中,都有**類**和**對象**的概念,其中洛搀,對象是類的實(shí)例倘潜,是通過類定義的結(jié)構(gòu)生成出來的格遭。而在 Objective-C 中愚铡,類本身也是一個對象,類作為對象時(shí)的 isa 指針指向的是元類(Meta Class)幔摸,這個我們后面再說摸柄。

super_class,指向該類的父類既忆,如果該類已經(jīng)是根類(NSObject 或 NSProxy)驱负,則 super_class 為 NULL。

cache尿贫,用于緩存最近使用的方法电媳。一個對象可響應(yīng)的方法列表中通常只有一部分是經(jīng)常被調(diào)用的踏揣,cache 則是用來緩存最常調(diào)用的方法庆亡,從而避免每次方法調(diào)用時(shí)都去查找對象的整個方法列表。并且捞稿,在一些結(jié)構(gòu)較為復(fù)雜的類關(guān)系中又谋,一個對象的響應(yīng)方法可能來自于它繼承的類結(jié)構(gòu)中拼缝,那么查找相應(yīng)的響應(yīng)方法時(shí)就會比較耗時(shí),通過 cache 緩存也能降低查找時(shí)間彰亥。

version咧七,根據(jù)這個字段可以獲得類的版本信息,在對象的序列化中可以通過類的版本信息來標(biāo)識出不同版本的類定義中實(shí)例變量布局的改變任斋。

元類(Meta Class)

上面講到一個類也是一個對象继阻,那么它必然也是某一種類的實(shí)例,這種類就是:元類(Meta Class)废酷。就如類是對應(yīng)的實(shí)例的描述一樣瘟檩,元類則是類作為對象時(shí)的描述。元類的方法列表對應(yīng)的則是類方法(Class Method)列表澈蟆,這正是類作為一個對象時(shí)所需要的墨辛。當(dāng)我們像[NSObject alloc]這樣給一個類發(fā)送消息時(shí),Runtime 就會去對應(yīng)的元類查找其類方法列表趴俘,并匹配調(diào)用睹簇。

Since a class is an object, it must be an instance of some other class: a metaclass. The metaclass is the description of the class object, just like the class is the description of ordinary instances. Class methods are described by the metaclass on behalf of the class object, just like instance methods are described by the class on behalf of the instance objects.

那在接著往下探究:元類又是誰的實(shí)例呢?它的 isa 又指向誰呢寥闪?答案如下圖所示太惠。

元類的 isa 都指向根元類(Root Meta Class),也就是說元類都是根元類(Root Meta Class)的實(shí)例橙垢。而根元類(Root Meta Class)的 isa 則指向自己垛叨,這樣就不會無休止的鏈下去了。

在圖中還能看到類的繼承關(guān)系以及對應(yīng)的元類的繼承關(guān)系柜某,已經(jīng)比較清晰了嗽元,不再詳述。

類的實(shí)例的數(shù)據(jù)結(jié)構(gòu)

在 Objective-C 中類的實(shí)例的數(shù)據(jù)結(jié)構(gòu)是定義在struct objc_object中(objc/objc.h):

// Represents aninstanceof a class.struct objc_object {? ? Class isa? OBJC_ISA_AVAILABILITY;};

可以看到喂击,這個結(jié)構(gòu)體只有一個字段剂癌,即指向該實(shí)例所屬類的 isa 指針。這個跟上面講的類的數(shù)據(jù)結(jié)構(gòu)中的 isa 略有不同:類的 isa 指向?qū)?yīng)的元類(Meta Class)翰绊,實(shí)例的 isa 則是指向?qū)?yīng)的類(Class)佩谷,而這個 Class 里就如上所講的包含了這個實(shí)例所屬類的各種信息:父類、類名监嗜、方法列表等等谐檀。

在我們向一個類的實(shí)例發(fā)送消息時(shí),Runtime 會根據(jù)實(shí)例對象的 isa 指針找到這個實(shí)例對象所屬的類裁奇,接著再在這個類的方法列表和其父類的方法列表中查找與消息對應(yīng)的 selector 指向的方法桐猬,然后執(zhí)行它。

當(dāng)創(chuàng)建某一個類的實(shí)例時(shí)刽肠,分配的內(nèi)存中會包含一個 objc_object 數(shù)據(jù)結(jié)構(gòu)溃肪,然后是類的實(shí)例變量的數(shù)據(jù)免胃。

我們常見的 id 是一個 struct objc_object 類型的指針。id 類型的對象可以轉(zhuǎn)換為任何一種類型的對象惫撰,它的作用有點(diǎn)類似 C 語言中的 void * 指針類型羔沙。

// A pointer to aninstanceof a class.typedef struct objc_object *id;

運(yùn)行時(shí)操作類與對象的代碼示例

實(shí)例、類厨钻、父類扼雏、元類關(guān)系結(jié)構(gòu)的示例代碼

首先我創(chuàng)建了繼承關(guān)系為SubClass -> SuperClass -> NSObject的幾個類,下面就用 Runtime 提供的運(yùn)行時(shí)方法來打印一下相關(guān)信息:

#import #import"SuperClass.h"#import"SubClass.h"-(void)aboutClass {// Use "object_getClass()" to get "isa".SubClass *sub = [[SubClassalloc] init];NSLog(@"%@, %@", object_getClass(sub), class_getSuperclass(object_getClass(sub))); // Print: SubClass, SuperClassClasscls= objc_getMetaClass("SubClass");if(class_isMetaClass(cls)) {? ? ? ? NSLog(@"YES, %@, %@, %@",cls, class_getSuperclass(cls), object_getClass(cls)); // Print: YES, SubClass, SuperClass, NSObject}else{? ? ? ? NSLog(@"NO");}? ? SuperClass *sup = [[SuperClassalloc] init];NSLog(@"%@, %@", object_getClass(sup), class_getSuperclass(object_getClass(sup))); // Print: SuperClass, NSObjectcls= objc_getMetaClass("SuperClass");if(class_isMetaClass(cls)) {? ? ? ? NSLog(@"YES, %@, %@, %@",cls, class_getSuperclass(cls), object_getClass(cls)); // Print: YES, SuperClass, NSObject, NSObject}else{? ? ? ? NSLog(@"NO");}cls= objc_getMetaClass("UIView");if(class_isMetaClass(cls)) {? ? ? ? NSLog(@"YES, %@, %@, %@",cls, class_getSuperclass(cls), object_getClass(cls)); // Print: YES, UIView, UIResponder, NSObject}else{? ? ? ? NSLog(@"NO");}cls= objc_getMetaClass("NSObject");if(class_isMetaClass(cls)) {? ? ? ? NSLog(@"YES, %@, %@, %@",cls, class_getSuperclass(cls), object_getClass(cls)); // Print: YES, NSObject, NSObject, NSObject}else{? ? ? ? NSLog(@"NO");}}

打印信息如下:

SubClass, SuperClassYES, SubClass, SuperClass,NSObjectSuperClass,NSObjectYES, SuperClass,NSObject,NSObjectYES,UIView,UIResponder,NSObjectYES,NSObject,NSObject,NSObject

這里需要注意的是:object_getClass()可以獲得當(dāng)前對象isa夯膀。這里以 SubClass 相關(guān)的打印信息為例呢蛤,來解釋一下:

SubClass,SuperClassYES,SubClass,SuperClass, NSObject

首先我們通過 object_getClass() 獲取實(shí)例 sub 所屬的 Class(isa) 是SubClass;通過 class_getSuperclass() 我們可以獲取 SubClass 對應(yīng)的父類是SuperClass棍郎;通過 objc_getMetaClass() 指定類名其障,我們可以獲取對應(yīng)的元類,通過 class_isMetaClass() 我們可以判斷一個 Class 是否為元類涂佃,這里確認(rèn)后励翼,打出YES;接著辜荠,打印元類類名是SubClass汽抚;打印元類的父類是SuperClass;再通過 object_getClass() 獲得元類的 isa伯病,是NSObject造烁。

對于 SuperClass 和 UIView 的相關(guān)打印信息解釋也同理。從這些的打印信息可以看出午笛,與前文中給出的圖中的關(guān)系結(jié)構(gòu)是一致的惭蟋。

動態(tài)操作類與實(shí)例的示例代碼

接著上面的代碼,我們繼續(xù):

int32_t testRuntimeMethodIMP(id self,SEL_cmd,NSDictionary*dic) {NSLog(@"testRuntimeMethodIMP: %@", dic);? ? //Print:? ? // testRuntimeMethodIMP: {? ? //? ? a ="para_a";? ? //? ? b ="para_b";? ? // }return99;}- (void)runtimeConstruct {#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wundeclared-selector"http://1:Createandregister class, addmethodto class.Classcls = objc_allocateClassPair(SuperClass.class,"RuntimeSubClass",0);? ? //Methodreturns:"int32_t"; accepts:"id self","SEL _cmd","NSDictionary *dic".Souse"i@:@"here.? ? class_addMethod(cls, @selector(testRuntimeMethod), (IMP) testRuntimeMethodIMP,"i@:@");? ? //Youcan only register a class once.? ? objc_registerClassPair(cls);? ? //2:Createinstanceofclass, print some info about classandassociated meta class.? ? id sub = [[cls alloc] init];NSLog(@"%@, %@", object_getClass(sub), class_getSuperclass(object_getClass(sub))); //Print:RuntimeSubClass,SuperClassClassmetaCls = objc_getMetaClass("RuntimeSubClass");if(class_isMetaClass(metaCls)) {NSLog(@"YES, %@, %@, %@", metaCls, class_getSuperclass(metaCls), object_getClass(metaCls)); //Print:YES,RuntimeSubClass,SuperClass,NSObject}else{NSLog(@"NO");? ? }? ? //3:Methodsofclass.? ? unsignedintoutCount =0;Method*methods = class_copyMethodList(cls, &outCount);for(int32_t i =0; i < outCount; i++) {Methodmethod= methods[i];NSLog(@"%@, %s",NSStringFromSelector(method_getName(method)), method_getTypeEncoding(method));? ? }? ? //Print: testRuntimeMethod, i@:@? ? free(methods);? ? //4:Callmethod.? ? int32_tresult= (int) [sub performSelector:@selector(testRuntimeMethod) withObject:@{@"a":@"para_a", @"b":@"para_b"}];NSLog(@"%d",result); //Print:99//5:Destoryinstancesandclass.? ? //Destroyinstancesofcls class before destroy cls class.? ? sub =nil;? ? //Donotcall this functionifinstancesofthe cls classoranysubclass exist.? ? objc_disposeClassPair(cls);#pragma clang diagnostic pop}

執(zhí)行上面代碼得到的打印信息如下:

RuntimeSubClass, SuperClassYES, RuntimeSubClass, SuperClass, NSObjecttestRuntimeMethod, i@:@testRuntimeMethodIMP:{? ? a ="para_a";b="para_b";}99

在上面的代碼中药磺,我們在運(yùn)行時(shí)動態(tài)創(chuàng)建了 SuperClass 的一個子類:RuntimeSubClass告组;接著為這個類添加了方法和實(shí)現(xiàn);打印了 RuntimeSubClass 的類癌佩、父類木缝、元類相關(guān)信息;遍歷和打印了 RuntimeSubClass 的方法的相關(guān)信息围辙;調(diào)用了 RuntimeSubClass 的方法我碟;最后銷毀了實(shí)例和類。

上面代碼中姚建,有幾點(diǎn)在這里說明一下:

我們看到了幾行#pragma clang diagnostic...代碼矫俺,這是用于忽略編譯器對于未聲明的 @selector 的 warning。因?yàn)槲覀兊拇a中我們需要動態(tài)的為一個類創(chuàng)建方法,所以必然不會事先聲明恳守。

class_addMethod()函數(shù)的最后一個參數(shù) types 是描述方法返回值和參數(shù)列表的字符串,我們的代碼中的用到的i@:@四個字符分別對應(yīng)著:返回值 int32_t贩虾、參數(shù) id self催烘、參數(shù) SEL _cmd、參數(shù) NSDictionary *dic缎罢。這個其實(shí)就是類型編碼(Type Encoding)的概念伊群。在 Objective-C 中,為了協(xié)助 Runtime 系統(tǒng)策精,編譯器會將每個方法的返回值和參數(shù)列表編碼為一個字符串舰始,這個字符串會與方法對應(yīng)的 selector 關(guān)聯(lián)。更詳細(xì)的知識可以查閱 [Type Encodings][6]咽袜。

使用objc_registerClassPair()函數(shù)需要注意丸卷,你不能注冊已經(jīng)注冊過的類。

使用objc_disposeClassPair()函數(shù)需要注意询刹,如果一個類的實(shí)例和子類還存在時(shí)谜嫉,不要去銷毀一個類。

關(guān)于更多 Runtime 函數(shù)的使用細(xì)節(jié)可以查閱Objective-C Runtime Reference凹联。

運(yùn)行時(shí)的成員變量與屬性

相關(guān)函數(shù)

Runtime 中與成員變量和屬性相關(guān)的函數(shù)有很多沐兰,這里列出一些:

Ivar class_getClassVariable(Class cls, const char *name),返回指定類的指定名字的成員變量蔽挠。

Ivar *class_copyIvarList(Class cls, unsigned int *outCount)住闯,返回指定類的成員變量列表。調(diào)用后需要自己 free()澳淑。

BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)比原,給指定的類添加成員變量。這個函數(shù)只能在 objc_allocateClassPair() 和 objc_registerClassPair() 之間調(diào)用杠巡,并且不能為一個已經(jīng)存在的類添加成員變量春寿。

id object_getIvar(id obj, Ivar ivar),獲得對象的指定成員變量的值忽孽。速度比 object_getInstanceVariable() 快绑改。

void object_setIvar(id obj, Ivar ivar, id value),設(shè)置對象指定成員變量的值兄一。速度比 object_setInstanceVariable() 快厘线。

Ivar object_getInstanceVariable(id obj, const char *name, void **outValue),獲取指定名字的成員變量的值出革。

Ivar object_setInstanceVariable(id obj, const char *name, void *value)造壮,設(shè)置指定名字成員變量的值。

const char *ivar_getName(Ivar v),獲取成員變量名耳璧。

const char *ivar_getTypeEncoding(Ivar v)成箫,獲取成員變量的類型編碼。

ptrdiff_t ivar_getOffset(Ivar v)旨枯,獲取成員變量的偏移量蹬昌。

objc_property_t class_getProperty(Class cls, const char *name), 獲取指定類指定名字的屬性。

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount), 獲取指定類的屬性列表攀隔。調(diào)用后需要自己 free()皂贩。

BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount), 給指定的類添加屬性昆汹。

void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)明刷,替代指定類的屬性。

const char *property_getName(objc_property_t property)满粗,獲取屬性名辈末。

const char *property_getAttributes(objc_property_t property),獲取屬性特性描述映皆。

objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount)本冲,獲取屬性特性列表。調(diào)用后需要自己 free()劫扒。

char *property_copyAttributeValue(objc_property_t property, const char *attributeName)檬洞,獲取屬性特性值。調(diào)用后需要自己 free()沟饥。

成員變量(Ivar)的數(shù)據(jù)結(jié)構(gòu)

在 Objective-C 中成員變量即Ivar類型添怔,是指向struct objc_ivar結(jié)構(gòu)體的指針∠涂酰可以在 objc/runtime.h 中查到:

typedefstructobjc_ivar *Ivar;

而 struct objc_ivar 結(jié)構(gòu)體的數(shù)據(jù)結(jié)構(gòu)如下:

structobjc_ivar {char*ivar_name OBJC2_UNAVAILABLE;// 變量名广料。char*ivar_type OBJC2_UNAVAILABLE;// 變量類型。intivar_offset OBJC2_UNAVAILABLE;// 基地址偏移量幼驶,在對成員變量尋址時(shí)使用艾杏。#ifdef __LP64__intspace OBJC2_UNAVAILABLE;#endif}

屬性的數(shù)據(jù)結(jié)構(gòu)

屬性(Property)的數(shù)據(jù)結(jié)構(gòu):

typedefstructobjc_property *objc_property_t;

屬性特性(Attribute)的數(shù)據(jù)結(jié)構(gòu):

/// Defines a property attributetypedefstruct{constchar*name;/**< The name of the attribute */constchar*value;/**< The value of the attribute (usually empty) */}objc_property_attribute_t;

屬性和成員變量的聯(lián)系

本質(zhì)上一個屬性背后必然對應(yīng)著一個成員變量,但是屬性又不僅僅只是一個成員變量盅藻,屬性還會根據(jù)自己對應(yīng)的屬性特性的定義來對這個成員變量進(jìn)行一系列的封裝:提供 Getter/Setter 方法购桑、內(nèi)存管理策略、線程安全機(jī)制等等氏淑。

運(yùn)行時(shí)操作成員變量和屬性的代碼示例

接著放示例代碼:

NSString * runtimePropertyGetterIMP(id self, SEL _cmd) {? ? Ivar ivar = class_getInstanceVariable([self class],"_runtimeProperty");returnobject_getIvar(self, ivar);}void runtimePropertySetterIMP(id self, SEL _cmd, NSString *s) {? ? Ivar ivar = class_getInstanceVariable([self class],"_runtimeProperty");NSString *old = (NSString *) object_getIvar(self, ivar);if(![old isEqualToString:s]) {? ? ? ? object_setIvar(self, ivar, s);}}- (void)aboutIvarAndProperty {#pragma clang diagnostic push#pragma clang diagnostic ignored"-Wundeclared-selector"http:// 1: Add property and getter/setter.Classcls= objc_allocateClassPair(SuperClass.class,"RuntimePropertySubClass",0);BOOL b = class_addIvar(cls,"_runtimeProperty", sizeof(cls), log2(sizeof(cls)), @encode(NSString));NSLog(@"%@", b ? @"YES": @"NO"); // Print: YESobjc_property_attribute_t type ={"T", "@\"NSString\""};objc_property_attribute_t ownership ={"C", ""}; // C = copyobjc_property_attribute_t isAtomic ={"N", ""}; // N = nonatomicobjc_property_attribute_t backingivar? ={"V", "_runtimeProperty"};objc_property_attribute_t attrs[] = {type, ownership, isAtomic, backingivar};class_addProperty(cls,"runtimeProperty", attrs,4);class_addMethod(cls, @selector(runtimeProperty), (IMP) runtimePropertyGetterIMP,"@@:");class_addMethod(cls, @selector(setRuntimeProperty), (IMP) runtimePropertySetterIMP,"v@:@");// You can only register a class once.objc_registerClassPair(cls);// 2: Print all properties.unsignedintoutCount =0;objc_property_t *properties = class_copyPropertyList(cls, &outCount);for(int32_t i =0; i < outCount; i++) {objc_property_t property = properties[i];NSLog(@"%s, %s\n", property_getName(property), property_getAttributes(property));}// Print:// runtimeProperty, T@"NSString",C,N,V_runtimePropertyfree(properties);// 3: Print all ivars.Ivar *ivars = class_copyIvarList(cls, &outCount);for(int32_t i =0; i < outCount; i++) {Ivar ivar = ivars[i];NSLog(@"%s, %s\n", ivar_getName(ivar), ivar_getTypeEncoding(ivar));}// Print:// _runtimeProperty, {NSString=#}free(ivars);// 4: Use runtime property.id sub = [[clsalloc] init];[sub performSelector:@selector(setRuntimeProperty) withObject:@"It-is-a-runtime-property."];NSString *s = [sub performSelector:@selector(runtimeProperty)]; //[sub valueForKey:@"runtimeProperty"];NSLog(@"%@", s); // Print: It-is-a-runtime-property.// 5: Clear.// Destroy instances of cls class before destroy cls class.sub = nil;// Do not call this function if instances of the cls class or any subclass exist.objc_disposeClassPair(cls);#pragma clang diagnostic pop}

上面代碼的打印信息如下:

YESruntimeProperty, T@"NSString",C,N,V_runtimeProperty_runtimeProperty, {NSString=#}It-is-a-runtime-property.

上面的代碼中勃蜘,我們在運(yùn)行時(shí)動態(tài)創(chuàng)建了 SuperClass 的一個子類RuntimePropertySubClass;然后為它動態(tài)添加了 Ivar:_runtimeProperty假残、對應(yīng)的 Property:runtimeProperty缭贡、對應(yīng)的 Getter/Setter:runtimePropertysetRuntimeProperty;接著我們遍歷和打印了 RuntimePropertySubClass 的 Ivar 列表和 Property 列表;然后創(chuàng)建了 RuntimePropertySubClass 的一個實(shí)例sub阳惹,并使用了 Property谍失;最后我們清理了 sub 和 RuntimePropertySubClass。

這里有幾點(diǎn)需要注意的:

我們不能用 class_addIvar() 函數(shù)為一個已經(jīng)存在的類添加 Ivar莹汤。并且 class_addIvar() 只能在 objc_allocateClassPair() 和 objc_registerClassPair() 之間調(diào)用快鱼。

添加屬性特性時(shí)的各種類型字符可以參考:[Property Type String][8]。

添加一個屬性及對應(yīng)的成員變量后体啰,我們還能通過[obj valueForKey:@"propertyName"];獲得屬性值。

運(yùn)行時(shí)的消息分發(fā)

相關(guān)函數(shù)

id objc_msgSend(id self, SEL op, ...)嗽仪,消息分發(fā)荒勇。(objc/message.h)

id method_invoke(id receiver, Method m, ...);,調(diào)用指定方法的實(shí)現(xiàn)闻坚。

void method_invoke_stret(id receiver, Method m, ...);沽翔,調(diào)用返回一個數(shù)據(jù)結(jié)構(gòu)的方法的實(shí)現(xiàn)。

SEL method_getName(Method m);窿凤,獲取方法名仅偎。

IMP method_getImplementation(Method m);,返回方法的實(shí)現(xiàn)雳殊。

const char * method_getTypeEncoding(Method m);橘沥,獲取描述方法參數(shù)和返回值類型的字符串。

char * method_copyReturnType(Method m);夯秃,獲取方法的返回值類型的字符串座咆。

char * method_copyArgumentType(Method m, unsigned int index);,獲取方法的指定位置參數(shù)的類型字符串仓洼。

void method_getReturnType(Method m, char *dst, size_t dst_len);介陶,通過引用返回方法的返回值類型字符串。

unsigned int method_getNumberOfArguments(Method m);色建,返回方法的參數(shù)的個數(shù)哺呜。

void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len);,通過引用返回方法指定位置參數(shù)的類型字符串箕戳。

struct objc_method_description * method_getDescription(Method m);某残,返回指定方法的方法描述結(jié)構(gòu)體。

IMP method_setImplementation(Method m, IMP imp);陵吸,設(shè)置方法的實(shí)現(xiàn)驾锰。注意該函數(shù)返回值是方法之前的實(shí)現(xiàn)。

void method_exchangeImplementations(Method m1, Method m2);走越,交換兩個方法的實(shí)現(xiàn)椭豫。

const char * sel_getName(SEL sel);,返回給定選擇器指定的方法的名稱。

SEL sel_registerName(const char *str);赏酥,在Objective-C Runtime系統(tǒng)中注冊一個方法喳整,將方法名映射到一個選擇器,并返回這個選擇器裸扶。

SEL sel_getUid(const char *str);框都,在Objective-C Runtime系統(tǒng)中注冊一個方法。

BOOL sel_isEqual(SEL lhs, SEL rhs);呵晨,比較兩個選擇器魏保。

消息機(jī)制相關(guān)的數(shù)據(jù)結(jié)構(gòu)

選擇器

選擇器在 Objective-C 中即 SEL 類型。它的定義如下(objc/objc.h):

typedefstructobjc_selector *SEL;

表示一個方法 selector 的指針摸屠。selector 用于表示運(yùn)行時(shí)方法的名字谓罗,Objective-C 在編譯時(shí)會根據(jù)每個方法的名字為方法生成一個唯一的整型標(biāo)識來替代方法名,這個整型標(biāo)識就是 SEL季二。比如:

SEL sel = @selector(alloc);NSLog(@"%p", sel); // Print: 0x10338b545

只要方法名相同檩咱,即使方法是否在不同的類中,它生成的 SEL 也是一樣的胯舷。所以刻蚯,SEL 沒干啥,它就是唯一標(biāo)識一個方法名而已桑嘶。

這樣看來炊汹,在一個類中是不能存在兩個同名的方法的,即使參數(shù)類型不同也不行逃顶。比如下面的兩個方法放在同一個類中是無法編譯通過的:

- (void)test:(int)i;- (int)test:(double)d;

這個很好理解:一個類的實(shí)例去調(diào)用方法時(shí)會通過方法的 SEL 去映射兵扬,如果存在相同的 SEL,那就不知道調(diào)用誰了口蝠。

當(dāng)然器钟,不同的類是可以有相同的 SEL 的,即使這些類之間是繼承關(guān)系也沒問題妙蔗。這是因?yàn)椴煌念惏涟裕麄冋{(diào)用方法時(shí)的對象實(shí)例是不一樣的,那么對應(yīng)的方法列表和 SEL 就是不一樣的眉反,不會有沖突昙啄。就算存在繼承關(guān)系,也是先找子類的方法列表寸五,沒有時(shí)再找父類的方法列表梳凛,也不會有問題。

在一個工程中梳杏,所有的 SEL 會組成一個 Set 集合韧拒,這就意味著不會有重復(fù)的 SEL淹接,這個也是可以理解的,首先這樣可以去掉重復(fù)的 SEL(不同類的同名方法生成的相同 SEL)叛溢,從而降低集合大小塑悼、提高查找性能;其次楷掉,調(diào)用時(shí)根據(jù)對象去查找對應(yīng)的方法列表厢蒜,也不會有問題。

所以綜上所述烹植,SEL 確實(shí)沒干啥斑鸦,它就是唯一標(biāo)識一個方法名而已。

在編譯時(shí)草雕,我們通過 @selector 來獲取指定方法名的 SEL:

SELaSelector =@selector(methodName);

在運(yùn)行時(shí)巷屿,我們通過 NSSelectorFromString() 來獲得指定方法名的 SEL:

SEL aSelector = NSSelectorFromString(@"methodName");

函數(shù)指針

函數(shù)指針在 Objective-C 中即 IMP 類型。它的定義如下(objc/objc.h):

typedef id (*IMP)(id,SEL, ...);

IMP 其實(shí)就是implementation的縮寫促绵,表示方法實(shí)現(xiàn)的代碼塊地址攒庵,可以像 C 函數(shù)一樣直接調(diào)用嘴纺。通常情況下败晴,我們都是通過[object method:parameter]或objc_msgSend()的方式調(diào)用方法或函數(shù),然后 Runtime 去尋找消息匹配的 IMP 來調(diào)用栽渴,但有時(shí)候我們也可以直接獲取 IMP 來調(diào)用尖坤。通過 IMP,我們可以跳過 Rumtime 的消息分發(fā)流程闲擦,直接執(zhí)行 IMP 指向的代碼塊慢味,這樣會比直接向?qū)ο蟀l(fā)送消息高效一些。這就是 IMP Caching 技術(shù)墅冷。

方法

方法在 Objective-C 中即 Method 類型纯路。它的定義如下(objc/runtime.h):

typedef struct objc_method *Method;struct objc_method{

SEL method_name OBJC2_UNAVAILABLE;

char *method_types OBJC2_UNAVAILABLE;

IMP method_imp OBJC2_UNAVAILABLE;

}OBJC2_UNAVAILABLE;

我們可以看到 Method 是struct objc_method指針類型。struct objc_method 結(jié)構(gòu)中包含了一個 SEL 和一個 IMP寞忿,實(shí)際上是做了一個 SEL 到 IMP 的映射驰唬,有了 SEL 我們就可以找到對應(yīng)的 IMP 來調(diào)用對應(yīng)的代碼。

此外腔彰,還有一個方法描述的數(shù)據(jù)結(jié)構(gòu)如下:

struct objc_method_description {SELname; /**

這里的描述了方法的 SEL 以及參數(shù)類型叫编。

消息分發(fā)機(jī)制說明

Objective-C 中的消息是直到運(yùn)行時(shí)才綁定到具體的方法實(shí)現(xiàn)上∨祝基本上代碼中形如[receiver message]的消息表達(dá)式在編譯階段只是確定了要向 receiver 發(fā)送 message 這樣一件事搓逾,這里的 message 是一個方法名 selector 以及相關(guān)的參數(shù)。這個消息會被編譯器轉(zhuǎn)化為對objc_msgSend()函數(shù)或相近函數(shù)的調(diào)用杯拐,在這個過程中霞篡,objc_msgSend() 函數(shù)會獲取 receiver世蔗、selector 以及 message 中的參數(shù)作為自己的參數(shù),調(diào)用形式如下:

objc_msgSend(receiver, selector, arg1, arg2, ...)

這個消息分發(fā)函數(shù)會在運(yùn)行時(shí)完成動態(tài)綁定相關(guān)的事情寇损,大體步驟如下:

首先找到 selector 對應(yīng)的確切的實(shí)現(xiàn)例程凸郑。由于同一個方法名可能會因?yàn)樗诘念惒煌鴮?shí)現(xiàn)不同,所以它對應(yīng)的確切的實(shí)現(xiàn)要依賴于類和接受者 receiver矛市。

調(diào)用對應(yīng)的實(shí)現(xiàn)例程芙沥,傳入 receiver 對象以及相關(guān)參數(shù)。

最后浊吏,將實(shí)現(xiàn)例程的返回值作為自己的返回值傳遞出去而昨。

消息分發(fā)的奧義隱藏在編譯器為每個類和對象構(gòu)造的數(shù)據(jù)結(jié)構(gòu)中,這個在我們前面的章節(jié)中以及介紹過了(struct objc_object 和 struct objc_class)找田。其中最關(guān)鍵的兩個字段:

指向父類的指針(isa)歌憨。

類的分發(fā)表(methodLists)。

消息分發(fā)的流程如圖所示:

當(dāng)消息發(fā)送給一個對象時(shí)墩衙,objc_msgSend 通過對象的 isa 指針獲取到類的結(jié)構(gòu)體务嫡,然后在方法分發(fā)表里面查找方法的 selector。如果沒有找到 selector漆改,則通過 objc_msgSend 結(jié)構(gòu)體中的指向父類的指針找到其父類心铃,并在父類的分發(fā)表里面查找方法的 selector。依此挫剑,會一直沿著類的繼承體系到達(dá) NSObject 類去扣。一旦定位到 selector,函數(shù)會就獲取到了實(shí)現(xiàn)的入口點(diǎn)樊破,并傳入相應(yīng)的參數(shù)來執(zhí)行方法的具體實(shí)現(xiàn)愉棱。如果最后沒有定位到 selector,則會走消息轉(zhuǎn)發(fā)流程哲戚。

此外奔滑,為了加速消息的處理,運(yùn)行時(shí)系統(tǒng)會緩存使用過的 selector 及對應(yīng)的方法的地址顺少。這點(diǎn)在前面已經(jīng)討論過朋其,不再重復(fù)。

運(yùn)行時(shí)消息分發(fā)的代碼示例

Method Swizzling

在前文中講 Method 的數(shù)據(jù)結(jié)構(gòu)時(shí)我們說到過祈纯,方法的數(shù)據(jù)結(jié)構(gòu)中包含了 SEL 和 IMP令宿。selector 相當(dāng)于一個方法的 id;IMP 是方法的實(shí)現(xiàn)腕窥。這樣分開的一個便利之處是 selector 和 IMP 之間的對應(yīng)關(guān)系可以被改變粒没。比如一個 IMP 可以有多個 selectors 指向它。而本節(jié)所講的 Method Swizzling 的概念則是交換兩個方法的實(shí)現(xiàn)簇爆,從而「貍貓換太子」癞松。

UIViewController+Runtime.m

#import"UIViewController+Runtime.h"#import@implementationUIViewController(Runtime)+ (void)load {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? ? Class aClass = [selfclass];? ? ? ? SEL originalSelector =@selector(viewWillAppear:);? ? ? ? SEL swizzledSelector =@selector(xxx_viewWillAppear:);? ? ? ? Method originalMethod = class_getInstanceMethod(aClass, originalSelector);? ? ? ? Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);// When swizzling a class method, use the following:// Class aClass = object_getClass((id)self);// ...// Method originalMethod = class_getClassMethod(aClass, originalSelector);// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);BOOLdidAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if(didAddMethod) {? ? ? ? ? ? class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));? ? ? ? }else{? ? ? ? ? ? method_exchangeImplementations(originalMethod, swizzledMethod);? ? ? ? }? ? });}#pragma mark - Method Swizzling- (void)xxx_viewWillAppear:(BOOL)animated {NSLog(@"B1: %@",self);// Print: B1: [selfxxx_viewWillAppear:animated];NSLog(@"B2: %@",self);// Print: B2: }@end

ViewController.m

@implementationViewController#import"UIViewController+Runtime.h"- (void)viewWillAppear:(BOOL)animated {NSLog(@"A1: %@",self);// Print: A1: [superviewWillAppear:animated];NSLog(@"A2: %@",self);// Print: A2: }@end

在加載展示 ViewController 后爽撒,打印出的信息如下:

A1:B1:B2:A2:

上面的代碼有幾點(diǎn)需要說明的:

一般來說,Method Swizzling 應(yīng)該在一個類的+load方法實(shí)現(xiàn)响蓉。+load在一個類最開始被引用加載時(shí)就會調(diào)用硕勿。

使用 GCD 的dispatch_once來保證只調(diào)用一次,并且確保線程安全枫甲。

上面的代碼體現(xiàn)了兩個「分離」:

方法 SEL 和 IMP 的分離源武。方法的 SEL 和 IMP 可以綁定,也可以拆開重綁想幻。這是 Method Swizzling 的基礎(chǔ)粱栖。

對象和類的方法列表的分離。對象和類的方法列表的也是在運(yùn)行時(shí)根據(jù)類的結(jié)構(gòu)進(jìn)行動態(tài)綁定脏毯。

Method Swizzling 交換的是 UIViewController 類的-viewWillAppear:和-xxx_viewWillAppear:的實(shí)現(xiàn)闹究,對其子類并無影響。在代碼執(zhí)行的過程中食店,對象始終是 ViewController 的一個實(shí)例渣淤,只不過有時(shí)候它去調(diào)用了父類 UIViewController 的方法([super viewWillAppear:animated];)。代碼的執(zhí)行順序如圖所示:

消息轉(zhuǎn)發(fā)

當(dāng)一個對象能接收一個消息時(shí)吉嫩,就會走正常的方法調(diào)用流程价认。但如果一個對象無法接收指定消息時(shí),又會發(fā)生什么事呢率挣?默認(rèn)情況下刻伊,如果是以[receiver message]的方式調(diào)用方法露戒,如果 receiver 無法響應(yīng) message 消息時(shí)椒功,編譯器會報(bào)錯。但如果是以performSelector…的形式來調(diào)用智什,則需要等到運(yùn)行時(shí)才能確定 receiver 是否能接收 message 消息动漾。如果不能,則程序崩潰荠锭。

通常旱眯,當(dāng)我們不能確定一個對象是否能接收某個消息時(shí),會先調(diào)用respondsToSelector:來判斷一下:

if([self respondsToSelector:@selector(method)]) {? ? [self performSelector:@selector(method)];}

這里证九,我們想討論一下當(dāng)一個對象無法接收某一消息時(shí)的情況删豺。一般這個時(shí)候,就會啟動所謂消息轉(zhuǎn)發(fā)(message forwarding)機(jī)制愧怜,通過這一機(jī)制呀页,我們可以告訴對象如何處理未知的消息。默認(rèn)情況下拥坛,對象接收到未知的消息蓬蝶,會導(dǎo)致程序崩潰:

-[ViewControllermethod]: unrecognized selector sent to instance0x7fa09b784f40Terminatingapp due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewControllermethod]: unrecognized selector sent to instance0x7fa09b784f40'

這段異常信息實(shí)際上是由 NSObject 的doesNotRecognizeSelector方法拋出的尘分。不過,我們可以采取一些措施丸氛,讓我們的程序執(zhí)行特定的邏輯培愁,而避免程序的崩潰。

消息轉(zhuǎn)發(fā)機(jī)制基本上分為三個步驟:

第一步:動態(tài)方法解析缓窜。

第二步:備用接收者定续。

第三步:完整轉(zhuǎn)發(fā)。

第一步:動態(tài)方法解析

對象在接收到未知的消息時(shí)禾锤,首先會調(diào)用所屬類的類方法+resolveInstanceMethod:或者+resolveClassMethod:香罐,前者處理實(shí)例方法調(diào)用,后者處理類方法調(diào)用时肿。我們可以它們里面用class_addMethod()加入異常處理的方法庇茫,不過前提是我們以及實(shí)現(xiàn)了處理方法。示例代碼如下:

#import- (void)viewDidLoad {? ? [superviewDidLoad];? ? [selfperformSelector:@selector(unknownMethod)];}voiddealWithExceptionForUnknownMethod(idself, SEL _cmd) {NSLog(@"%@, %p",self, _cmd);// Print: , 0x1078259fc}+ (BOOL)resolveInstanceMethod:(SEL)sel {NSString*selectorString =NSStringFromSelector(sel);if([selectorString isEqualToString:@"unknownMethod"]) {? ? ? ? class_addMethod(self.class,@selector(unknownMethod), (IMP) dealWithExceptionForUnknownMethod,"v@:");? ? }return[superresolveInstanceMethod:sel];}

代碼打芋Τ伞:

,0x1078259fc

可以發(fā)現(xiàn)對unknownMethod方法的調(diào)用被截獲了并在dealWithExceptionForUnknownMethod函數(shù)中進(jìn)行了處理旦签,程序沒有再崩潰。

@dynamic屬性就可以用這種方案來實(shí)現(xiàn)寸宏。

第二步:備用接收者

如果在第一步還是無法處理消息宁炫,則 Runtime 會繼續(xù)調(diào)以下方法:

-(id)forwardingTargetForSelector:(SEL)aSelector

如果一個對象實(shí)現(xiàn)了這個方法,并返回一個非 nil 的結(jié)果氮凝,則這個對象會作為消息的新接收者羔巢,且消息會被分發(fā)到這個對象。當(dāng)然這個對象不能是 self 自身罩阵,否則就會出現(xiàn)無限循環(huán)竿秆。當(dāng)然,如果我們沒有指定相應(yīng)的對象來處理 aSelector稿壁,則應(yīng)該調(diào)用父類的實(shí)現(xiàn)來返回結(jié)果幽钢。示例代碼如下:

RuntimeMethodHelper.h

#import@interfaceRuntimeMethodHelper:NSObject- (void)unknownMethod2;@end

RuntimeMethodHelper.m

#import"RuntimeMethodHelper.h"@implementationRuntimeMethodHelper- (void)unknownMethod2 {NSLog(@"%@, %p",self, _cmd);// Print: , 0x10170d99a}@end

ViewController.m

#import- (void)viewDidLoad {? ? [superviewDidLoad];? ? [selfperformSelector:@selector(unknownMethod)];? ? [selfperformSelector:@selector(unknownMethod2)];}// Deal with unknownMethod.voiddealWithExceptionForUnknownMethod(idself, SEL _cmd) {NSLog(@"%@, %p",self, _cmd);// Print: , 0x1078259fc}+ (BOOL)resolveInstanceMethod:(SEL)sel {NSString*selectorString =NSStringFromSelector(sel);if([selectorString isEqualToString:@"unknownMethod"]) {? ? ? ? class_addMethod(self.class,@selector(unknownMethod), (IMP) dealWithExceptionForUnknownMethod,"v@:");? ? }return[superresolveInstanceMethod:sel];}// Deal with unknownMethod2.- (id)forwardingTargetForSelector:(SEL)aSelector {NSString*selectorString =NSStringFromSelector(aSelector);if([selectorString isEqualToString:@"unknownMethod2"]) {return[[RuntimeMethodHelper alloc] init];? ? }return[superforwardingTargetForSelector:aSelector];}

代碼打印信息:

,0x109c1f98c,0x109c1f99a

可以看到,對于unknownMethod方法已經(jīng)在第一步:動態(tài)方法解析中被+resolveInstanceMethod處理傅是,而unknownMethod2則放到了第二步:備用接收者才被處理匪燕。

這一步適用于當(dāng)我們只想將消息轉(zhuǎn)發(fā)到另一個能處理該消息的對象上的情況,它無法進(jìn)一步對消息進(jìn)行處理喧笔,比如:操作消息的參數(shù)和返回值帽驯。

第三步:完整轉(zhuǎn)發(fā)

如果第二步:備用接收者還是未能處理好消息,那么接下來只有啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了书闸,這時(shí)候會調(diào)用以下方法:

-(void)forwardInvocation:(NSInvocation *)anInvocation

運(yùn)行時(shí)系統(tǒng)會在這一步給消息接收者最后一次機(jī)會將消息轉(zhuǎn)發(fā)給其它對象尼变。對象會創(chuàng)建一個表示消息的 NSInvocation 對象,把與尚未處理的消息有關(guān)的全部細(xì)節(jié)都封裝在 anInvocation 中梗劫,包括:selector享甸、目標(biāo)(target)和參數(shù)截碴。我們可以在-forwardInvocation:方法中選擇將消息轉(zhuǎn)發(fā)給其它對象。

當(dāng)你實(shí)現(xiàn)了-forwardInvocation:方法蛉威,你有兩個任務(wù)需要完成:

定位可以響應(yīng)封裝在 anInvocation 中的消息的對象日丹。這個對象不需要能處理所有未知消息。

使用 anInvocation 作為參數(shù)蚯嫌,將消息發(fā)送到選中的對象哲虾。anInvocation 將會保留調(diào)用結(jié)果,運(yùn)行時(shí)系統(tǒng)會提取這一結(jié)果并將其發(fā)送到消息的原始發(fā)送者择示。

不過束凑,在這個方法中我們可以實(shí)現(xiàn)一些更復(fù)雜的功能,我們可以對消息的內(nèi)容進(jìn)行修改栅盲,比如追回一個參數(shù)等汪诉,然后再去觸發(fā)消息。另外谈秫,若發(fā)現(xiàn)某個消息不應(yīng)由本類處理扒寄,則應(yīng)調(diào)用父類的同名方法,以便繼承體系中的每個類都有機(jī)會處理此調(diào)用請求拟烫。

另外還有一個重要的問題是我們必須重寫下面方法:

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息轉(zhuǎn)發(fā)機(jī)制使用從這個方法中獲取的信息來創(chuàng)建 NSInvocation 對象该编。因此我們必須重寫這個方法,為給定的 selector 提供一個合適的方法簽名硕淑。

代碼示例如下:

RuntimeMethodHelper.h

#import@interfaceRuntimeMethodHelper:NSObject- (void)unknownMethod2;- (void)unknownMethod3;@end

RuntimeMethodHelper.m

#import"RuntimeMethodHelper.h"@implementationRuntimeMethodHelper- (void)unknownMethod2 {NSLog(@"%@, %p",self, _cmd);// Print: , 0x102d7991a}- (void)unknownMethod3 {NSLog(@"%@, %p",self, _cmd);// Print: , 0x102d79929}@end

ViewController.m

- (void)viewDidLoad {? ? [superviewDidLoad];? ? [selfperformSelector:@selector(unknownMethod)];? ? [selfperformSelector:@selector(unknownMethod2)];? ? ? ? ? [selfperformSelector:@selector(unknownMethod3)];}// Deal with unknownMethod.voiddealWithExceptionForUnknownMethod(id self, SEL _cmd) {? ? NSLog(@"%@, %p", self, _cmd);// Print: , 0x102d7990c}+ (BOOL)resolveInstanceMethod:(SEL)sel {? ? NSString *selectorString = NSStringFromSelector(sel);if([selectorStringisEqualToString:@"unknownMethod"]) {? ? ? ? class_addMethod(self.class,@selector(unknownMethod), (IMP) dealWithExceptionForUnknownMethod,"v@:");? ? }return[superresolveInstanceMethod:sel];}// Deal with unknownMethod2.- (id)forwardingTargetForSelector:(SEL)aSelector {? ? NSString *selectorString = NSStringFromSelector(aSelector);if([selectorStringisEqualToString:@"unknownMethod2"]) {return[[RuntimeMethodHelper alloc] init];? ? }return[superforwardingTargetForSelector:aSelector];}// Deal with unknownMethod3.- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {? ? NSMethodSignature *signature = [supermethodSignatureForSelector:aSelector];if(!signature) {if([RuntimeMethodHelperinstancesRespondToSelector:aSelector]) {? ? ? ? ? ? signature = [RuntimeMethodHelperinstanceMethodSignatureForSelector:aSelector];? ? ? ? }? ? }returnsignature;}- (void)forwardInvocation:(NSInvocation *)anInvocation {if([RuntimeMethodHelperinstancesRespondToSelector:anInvocation.selector]) {? ? ? ? [anInvocationinvokeWithTarget:[[RuntimeMethodHelper alloc] init]];? ? }}

代碼打印信息:

,0x102d7990c,0x102d7991a,0x102d79929

NSObject 的-forwardInvocation:方法實(shí)現(xiàn)只是簡單調(diào)用了-doesNotRecognizeSelector:方法咕别,它不會轉(zhuǎn)發(fā)任何消息褐墅。這樣米辐,如果不在以上所述的三個步驟中處理未知消息艘希,到了 NSObject 那則會引發(fā)一個異常。

從某種意義上來講半开,-forwardInvocation:就像一個未知消息的分發(fā)中心隔披,將這些未知的消息轉(zhuǎn)發(fā)給其它對象赃份〖挪穑或者也可以像一個運(yùn)輸站一樣將所有未知消息都發(fā)送給同一個接收對象。這取決于具體的實(shí)現(xiàn)抓韩。

消息轉(zhuǎn)發(fā)與多重繼承

回過頭來看上面第二步和第三步纠永,通過-forwardingTargetForSelector:和-forwardInvocation:這兩個方法我們可以允許一個對象與其它對象建立關(guān)系,以處理某些未知消息谒拴,而表面上看仍然是該對象在處理消息尝江。通過這種關(guān)系,我們可以模擬多重繼承的某些特性英上,讓對象可以繼承其它對象的特性來處理一些事情炭序。不過啤覆,這兩者間有一個重要的區(qū)別:多重繼承將不同的功能集成到一個對象中,它會讓對象變得過大惭聂,涉及的東西過多;而消息轉(zhuǎn)發(fā)將功能分解到獨(dú)立的小的對象中辜纲,并通過某種方式將這些對象連接起來笨觅,并做相應(yīng)的消息轉(zhuǎn)發(fā)。

不過消息轉(zhuǎn)發(fā)雖然類似于繼承耕腾,但 NSObject 的一些方法還是能區(qū)分兩者见剩。如respondsToSelector:和isKindOfClass:只能用于繼承體系,而不能用于轉(zhuǎn)發(fā)鏈扫俺。便如果我們想讓這種消息轉(zhuǎn)發(fā)看起來像是繼承苍苞,則可以重寫這些方法,如以下代碼所示:

- (BOOL)respondsToSelector:(SEL)aSelector {if([super respondsToSelector:aSelector]) {returnYES;? ? }else{? ? ? ? /* Here, test whethertheaSelector message can? ? *? ? ? ? * be forwardedtoanother objectandwhetherthat*? ? ? ? * object can respondtoit. Return YESifitcan.? */? ? }returnNO;? }



參考

Objective-C Runtime Reference

Classes and Metaclasses

Objective-C Runtime 運(yùn)行時(shí)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狼纬,一起剝皮案震驚了整個濱河市柒啤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌畸颅,老刑警劉巖担巩,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異没炒,居然都是意外死亡涛癌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門送火,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拳话,“玉大人,你說我怎么就攤上這事种吸∑埽” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵坚俗,是天一觀的道長镜盯。 經(jīng)常有香客問我,道長猖败,這世上最難降的妖魔是什么速缆? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮恩闻,結(jié)果婚禮上艺糜,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好破停,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布翅楼。 她就那樣靜靜地躺著,像睡著了一般真慢。 火紅的嫁衣襯著肌膚如雪犁嗅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天晤碘,我揣著相機(jī)與錄音褂微,去河邊找鬼。 笑死园爷,一個胖子當(dāng)著我的面吹牛宠蚂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播童社,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼求厕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扰楼?” 一聲冷哼從身側(cè)響起呀癣,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弦赖,沒想到半個月后项栏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹬竖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年沼沈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片币厕。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡列另,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出旦装,到底是詐尸還是另有隱情页衙,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布阴绢,位于F島的核電站店乐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏旱函。R本人自食惡果不足惜响巢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棒妨。 院中可真熱鬧,春花似錦、人聲如沸券腔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纷纫。三九已至枕扫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辱魁,已是汗流浹背烟瞧。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留染簇,地道東北人参滴。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像锻弓,于是被迫代替她去往敵國和親砾赔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容