Runtime

簡介

Runtime 又叫運行時,是一套底層的 C 語言 API,其為 iOS 內(nèi)部的核心之一,我們平時編寫的 OC 代碼,底層都是基于它來實現(xiàn)的。比如:

[receiver message];// 底層運行時會被編譯器轉(zhuǎn)化為:objc_msgSend(receiver, selector)// 如果其還有參數(shù)比如:[receiver message:(id)arg...];// 底層運行時會被編譯器轉(zhuǎn)化為:objc_msgSend(receiver, selector, arg1, arg2, ...)

以上你可能看不出它的價值,但是我們需要了解的是 Objective-C 是一門動態(tài)語言,它會將一些工作放在代碼運行時才處理而并非編譯時。也就是說,有很多類和成員變量在我們編譯的時是不知道的,而在運行時,我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運行。

因此,編譯器是不夠的,我們還需要一個運行時系統(tǒng)(Runtime system)來處理編譯后的代碼。

Runtime 基本是用 C 和匯編寫的,由此可見蘋果為了動態(tài)系統(tǒng)的高效而做出的努力。蘋果和 GNU 各自維護一個開源的 Runtime 版本,這兩個版本之間都在努力保持一致。

Runtime 的作用

Objc 在三種層面上與 Runtime 系統(tǒng)進行交互:

通過 Objective-C 源代碼

通過 Foundation 框架的 NSObject 類定義的方法

通過對 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:?檢查對象能否響應(yīng)指定的消息;

-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ù)語,他們都對應(yīng)著數(shù)據(jù)結(jié)構(gòu)。

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector 對方法名進行包裝,以便找到對應(yīng)的方法實現(xiàn)。它的數(shù)據(jù)結(jié)構(gòu)是:

typedefstruct objc_selector *SEL;

我們可以看出它是個映射到方法的 C 字符串,你可以通過 Objc 編譯器器命令@selector()?或者 Runtime 系統(tǒng)的?sel_registerName?函數(shù)來獲取一個?SEL?類型的方法選擇器。

注意:

不同類中相同名字的方法所對應(yīng)的 selector 是相同的,由于變量的類型不同,所以不會導(dǎo)致它們調(diào)用方法實現(xiàn)混亂。

id

id 是一個參數(shù)類型,它是指向某個類的實例的指針。定義如下:

typedefstruct objc_object *id;struct objc_object { Class isa; };

以上定義,看到?objc_object?結(jié)構(gòu)體包含一個 isa 指針,根據(jù) isa 指針就可以找到對象所屬的類。

注意:

isa 指針在代碼運行時并不總指向?qū)嵗龑ο笏鶎俚念愋停圆荒芤揽克鼇泶_定類型,要想確定類型還是需要用對象的?-class?方法。

PS:KVO 的實現(xiàn)機理就是將被觀察對象的 isa 指針指向一個中間類而不是真實類型。

Class

typedefstruct 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;constchar *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 不能添加屬性的原因。

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()?會去它元類中查找能夠響應(yīng)消息的方法實現(xiàn),如果找到了,就會對這個類對象執(zhí)行方法調(diào)用。


上圖實現(xiàn)是?super_class?指針,虛線時?isa?指針。而根元類的父類是?NSObject,isa指向了自己。而?NSObject?沒有父類。

最后?objc_class?中還有一個?objc_cache?,緩存,它的作用很重要,后面會提到。

Method

Method 代表類中某個方法的類型

typedefstruct objc_method *Method;struct objc_method {? ? SEL method_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;char *method_types? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;? ? IMP method_imp? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

}

objc_method?存儲了方法名,方法類型和方法實現(xiàn):

方法名類型為?SEL

方法類型?method_types?是個 char 指針,存儲方法的參數(shù)類型和返回值類型

method_imp?指向了方法的實現(xiàn),本質(zhì)是一個函數(shù)指針

Ivar

Ivar?是表示成員變量的類型。

typedefstruct 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_offset?是基地址偏移字節(jié)

IMP

IMP在objc.h中的定義是:

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

它就是一個函數(shù)指針,這是由編譯器生成的。當你發(fā)起一個 ObjC 消息之后,最終它會執(zhí)行的那段代碼,就是由這個函數(shù)指針指定的。而?IMP?這個函數(shù)指針就指向了這個方法的實現(xiàn)。

如果得到了執(zhí)行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面?Cache?中會提到。

你會發(fā)現(xiàn)?IMP?指向的方法與?objc_msgSend?函數(shù)類型相同,參數(shù)都包含?id?和?SEL?類型。每個方法名都對應(yīng)一個?SEL?類型的方法選擇器,而每個實例對象中的?SEL?對應(yīng)的方法實現(xiàn)肯定是唯一的,通過一組?id和?SEL?參數(shù)就能確定唯一的方法實現(xiàn)地址。

而一個確定的方法也只有唯一的一組?id?和?SEL?參數(shù)。

Cache

Cache 定義如下:

typedefstruct objc_cache *Cachestruct objc_cache {unsignedint mask/* total = mask + 1 */? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;unsignedint occupied? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;? ? Method buckets[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

};

Cache 為方法調(diào)用的性能進行優(yōu)化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應(yīng)的方法,因為每次都要查找效率太低了,而是優(yōu)先在 Cache 中查找。

Runtime 系統(tǒng)會把被調(diào)用的方法存到 Cache 中,如果一個方法被調(diào)用,那么它有可能今后還會被調(diào)用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 Cache 一樣。

Property

typedefstruct objc_property *Property;typedefstruct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList?和?protocol_copyPropertyList?方法獲取類和協(xié)議中的屬性:

objc_property_t *class_copyPropertyList(Class cls,unsignedint *outCount)objc_property_t *protocol_copyPropertyList(Protocol *proto,unsignedint *outCount)

注意:

返回的是屬性列表,列表中每個元素都是一個?objc_property_t?指針

#import@interfacePerson :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é)果如下:

2014-11-1011:27:28.473 test[2321:451525]32014-11-1011:27:28.473 test[2321:451525] name--------T@"NSString",&,N,V_name2014-11-1011:27:28.473 test[2321:451525] age--------Ti,N,V_age2014-11-1011:27:28.474 test[2321:451525] weight--------Td,N,V_weight

property_getName?用來查找屬性的名稱,返回 c 字符串。property_getAttributes?函數(shù)挖掘?qū)傩缘恼鎸嵜Q和?@encode?類型,返回 c 字符串。

objc_property_t class_getProperty(Class cls,constchar *name)objc_property_t protocol_getProperty(Protocol *proto,constchar *name,BOOL isRequiredProperty,BOOL isInstanceProperty)

class_getProperty?和?protocol_getProperty?通過給出屬性名在類和協(xié)議中獲得屬性的引用。

消息

一些 Runtime 術(shù)語講完了,接下來就要說到消息了。體會蘋果官方文檔中的 messages aren’t bound to method implementations until Runtime。消息直到運行時才會與方法實現(xiàn)進行綁定。

這里要清楚一點,objc_msgSend?方法看清來好像返回了數(shù)據(jù),其實objc_msgSend?從不返回數(shù)據(jù),而是你的方法在運行時實現(xiàn)被調(diào)用后才會返回數(shù)據(jù)。下面詳細敘述消息發(fā)送的步驟(如下圖):


首先檢測這個?selector?是不是要忽略。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù)。

檢測這個?selector?的 target 是不是?nil,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會 Crash,因為運行時會被忽略掉。

如果上面兩步都通過了,那么就開始查找這個類的實現(xiàn)?IMP,先從 cache 里查找,如果找到了就運行對應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼。

如果 cache 找不到就找類的方法列表中是否有對應(yīng)的方法。

如果類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類為止。

如果還找不到,就要開始進入動態(tài)方法解析了,后面會提到。

在消息的傳遞中,編譯器會根據(jù)情況在?objc_msgSend?,?objc_msgSend_stret?,?objc_msgSendSuper?,?objc_msgSendSuper_stret?這四個方法中選擇一個調(diào)用。如果消息是傳遞給父類,那么會調(diào)用名字帶有 Super 的函數(shù),如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時,會調(diào)用名字帶有 stret 的函數(shù)。

方法中的隱藏參數(shù)

疑問:

我們經(jīng)常用到關(guān)鍵字?self?,但是?self?是如何獲取當前方法的對象呢?

其實,這也是 Runtime 系統(tǒng)的作用,self?實在方法運行時被動態(tài)傳入的。

當?objc_msgSend?找到方法對應(yīng)實現(xiàn)時,它將直接調(diào)用該方法實現(xiàn),并將消息中所有參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏參數(shù):

接受消息的對象(self?所指向的內(nèi)容,當前方法的對象指針)

方法選擇器(_cmd?指向的內(nèi)容,當前方法的 SEL 指針)

因為在源代碼方法的定義中,我們并沒有發(fā)現(xiàn)這兩個參數(shù)的聲明。它們時在代碼被編譯時被插入方法實現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?/p>

這兩個參數(shù)中,?self更實用。它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑。

這時我們可能會想到另一個關(guān)鍵字?super?,實際上?super?關(guān)鍵字接收到消息時,編譯器會創(chuàng)建一個?objc_super?結(jié)構(gòu)體:

struct objc_super { id receiver; Classclass; };

這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類。?receiver?仍然是?self?本身,當我們想通過?[super class]?獲取父類時,編譯器其實是將指向?self?的?id?指針和?class?的 SEL 傳遞給了?objc_msgSendSuper?函數(shù)。只有在?NSObject?類中才能找到?class?方法,然后?class?方法底層被轉(zhuǎn)換為?object_getClass(), 接著底層編譯器將代碼轉(zhuǎn)換為?objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數(shù)是指向?self?的?id?指針,與調(diào)用?[self class]?相同,所以我們得到的永遠都是?self?的類型。因此你會發(fā)現(xiàn):

// 這句話并不能獲取父類的類型,只能獲取當前類的類型名NSLog(@"%@",NSStringFromClass([super class]));

獲取方法地址

NSObject?類中有一個實例方法:methodForSelector,你可以用它來獲取某個方法選擇器對應(yīng)的?IMP?,舉個例子:

void (*setter)(id, SEL,BOOL);int i;setter = (void (*)(id, SEL,BOOL))[target? ? methodForSelector:@selector(setFilled:)];for ( i =0 ; i <1000 ; i++ )? ? setter(targetList[i],@selector(setFilled:),YES);

當方法被當做函數(shù)調(diào)用時,兩個隱藏參數(shù)也必須明確給出,上面的例子調(diào)用了1000次函數(shù),你也可以嘗試給?target?發(fā)送1000次?setFilled:?消息會花多久。

雖然可以更高效的調(diào)用方法,但是這種做法很少用,除非時需要持續(xù)大量重復(fù)調(diào)用某個方法的情況,才會選擇使用以免消息發(fā)送泛濫。

注意:

methodForSelector:方法是由 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性

動態(tài)方法解析

你可以動態(tài)提供一個方法實現(xiàn)。如果我們使用關(guān)鍵字?@dynamic?在類的實現(xiàn)文件中修飾一個屬性,表明我們會為這個屬性動態(tài)提供存取方法,編譯器不會再默認為我們生成這個屬性的 setter 和 getter 方法了,需要我們自己提供。

@dynamic propertyName;

這時,我們可以通過分別重載?resolveInstanceMethod:?和?resolveClassMethod:?方法添加實例方法實現(xiàn)和類方法實現(xiàn)。

當 Runtime 系統(tǒng)在 Cache 和類的方法列表(包括父類)中找不到要執(zhí)行的方法時,Runtime 會調(diào)用?resolveInstanceMethod:?或?resolveClassMethod:?來給我們一次動態(tài)添加方法實現(xiàn)的機會。我們需要用?class_addMethod?函數(shù)完成向特定類添加特定方法實現(xiàn)的操作:

void dynamicMethodIMP(idself, SEL _cmd) {// implementation ....}@implementationMyClass+ (BOOL)resolveInstanceMethod:(SEL)aSEL{if (aSEL ==@selector(resolveThisMethodDynamically)) {? ? ? ? ? class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP,"v@:");returnYES;? ? }return [super resolveInstanceMethod:aSEL];}@end

上面的例子為?resolveThisMethodDynamically?方法添加了實現(xiàn)內(nèi)容,就是?dynamicMethodIMP?方法中的代碼。其中?"v@:"?表示返回值和參數(shù)

注意:

動態(tài)方法解析會在消息轉(zhuǎn)發(fā)機制侵入前執(zhí)行,動態(tài)方法解析器將會首先給予提供該方法選擇器對應(yīng)的?IMP?的機會。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機制,就讓?resolveInstanceMethod:?方法返回?NO。

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


重定向

消息轉(zhuǎn)發(fā)機制執(zhí)行前,Runtime 系統(tǒng)允許我們替換消息的接收者為其他對象。通過?- (id)forwardingTargetForSelector:(SEL)aSelector?方法。

- (id)forwardingTargetForSelector:(SEL)aSelector{if(aSelector ==@selector(mysteriousMethod:)){return alternateObject;? ? }return [super forwardingTargetForSelector:aSelector];

}

如果此方法返回?nil?或者?self,則會計入消息轉(zhuǎn)發(fā)機制(forwardInvocation:),否則將向返回的對象重新發(fā)送消息。

轉(zhuǎn)發(fā)

當動態(tài)方法解析不做處理返回?NO?時,則會觸發(fā)消息轉(zhuǎn)發(fā)機制。這時?forwardInvocation:?方法會被執(zhí)行,我們可以重寫這個方法來自定義我們的轉(zhuǎn)發(fā)邏輯:

- (void)forwardInvocation:(NSInvocation *)anInvocation{if ([someOtherObject respondsToSelector:? ? ? ? ? ? [anInvocation selector]])? ? ? ? [anInvocation invokeWithTarget:someOtherObject];else? ? ? ? [super forwardInvocation:anInvocation];

}

唯一參數(shù)是個?NSInvocation?類型的對象,該對象封裝了原始的消息和消息的參數(shù)。我們可以實現(xiàn)?forwardInvocation:?方法來對不能處理的消息做一些處理。也可以將消息轉(zhuǎn)發(fā)給其他對象處理,而不拋出錯誤。

注意:參數(shù)?anInvocation 是從哪來的?

在?forwardInvocation:?消息發(fā)送前,Runtime 系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector:?消息,并取到返回的方法簽名用于生成 NSInvocation 對象。所以重寫?forwardInvocation:的同時也要重寫?methodSignatureForSelector:?方法,否則會拋異常。

當一個對象由于沒有相應(yīng)的方法實現(xiàn)而無法相應(yīng)某消息時,運行時系統(tǒng)將通過?forwardInvocation:?消息通知該對象。每個對象都繼承了?forwardInvocation:?方法。但是,?NSObject?中的方法實現(xiàn)只是簡單的調(diào)用了?doesNotRecognizeSelector:。通過實現(xiàn)自己的?forwardInvocation:?方法,我們可以將消息轉(zhuǎn)發(fā)給其他對象。

forwardInvocation:?方法就是一個不能識別消息的分發(fā)中心,將這些不能識別的消息轉(zhuǎn)發(fā)給不同的接收對象,或者轉(zhuǎn)發(fā)給同一個對象,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息,因此沒有響應(yīng)也不會報錯。這一切都取決于方法的具體實現(xiàn)。

注意:

forwardInvocation:方法只有在消息接收對象中無法正常響應(yīng)消息時才會被調(diào)用。所以,如果我們向往一個對象將一個消息轉(zhuǎn)發(fā)給其他對象時,要確保這個對象不能有該消息的所對應(yīng)的方法。否則,forwardInvocation:將不可能被調(diào)用。

轉(zhuǎn)發(fā)和多繼承

轉(zhuǎn)發(fā)和繼承相似,可用于為 Objc 編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉(zhuǎn)發(fā)出去,就好像它把另一個對象中的方法接過來或者“繼承”過來一樣。


這使得在不同繼承體系分支下的兩個類可以實現(xiàn)“繼承”對方的方法,在上圖中?Warrior?和?Diplomat?沒有繼承關(guān)系,但是?Warrior?將?negotiate?消息轉(zhuǎn)發(fā)給了?Diplomat?后,就好似?Diplomat?是?Warrior?的超類一樣。

消息轉(zhuǎn)發(fā)彌補了 Objc 不支持多繼承的性質(zhì),也避免了因為多繼承導(dǎo)致單個類變得臃腫復(fù)雜。

轉(zhuǎn)發(fā)與繼承

雖然轉(zhuǎn)發(fā)可以實現(xiàn)繼承的功能,但是?NSObject?還是必須表面上很嚴謹,像?respondsToSelector:?和?isKindOfClass:?這類方法只會考慮繼承體系,不會考慮轉(zhuǎn)發(fā)鏈。

如果上圖中的?Warrior?對象被問到是否能響應(yīng)?negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )

? ? ...

回答當然是?NO, 盡管它能接受?negotiate?消息而不報錯,因為它靠轉(zhuǎn)發(fā)消息給?Diplomat?類響應(yīng)消息。

如果你就是想要讓別人以為?Warrior?繼承到了?Diplomat?的?negotiate?方法,你得重新實現(xiàn)?respondsToSelector:?和?isKindOfClass:?來加入你的轉(zhuǎn)發(fā)算法:

- (BOOL)respondsToSelector:(SEL)aSelector{if ( [super respondsToSelector: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:?中也應(yīng)該寫一份轉(zhuǎn)發(fā)算法。如果使用了協(xié)議,conformsToProtocol:?同樣也要加入到這一行列中。

如果一個對象想要轉(zhuǎn)發(fā)它接受的任何遠程消息,它得給出一個方法標簽來返回準確的方法描述?methodSignatureForSelector:,這個方法會最終響應(yīng)被轉(zhuǎn)發(fā)的消息。從而生成一個確定的?NSInvocation?對象描述消息和消息參數(shù)。這個方法最終響應(yīng)被轉(zhuǎn)發(fā)的消息。它需要像下面這樣實現(xiàn):

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector{NSMethodSignature* signature = [super methodSignatureForSelector:selector];if (!signature) {? ? ? signature = [surrogate methodSignatureForSelector:selector];? ? }return signature;

}

健壯的實例變量(Non Fragile ivars)

在 Runtime 的現(xiàn)行版本中,最大的特點就是健壯的實例變量了。當一個類被編譯時,實例變量的內(nèi)存布局就形成了,它表明訪問類的實例變量的位置。實例變量一次根據(jù)自己所占空間而產(chǎn)生位移:


上圖左是?NSObject?類的實例變量布局。右邊是我們寫的類的布局。這樣子有一個很大的缺陷,就是缺乏拓展性。哪天蘋果更新了?NSObject?類的話,就會出現(xiàn)問題:


我們自定義的類的區(qū)域和父類的區(qū)域重疊了。只有蘋果將父類改為以前的布局才能拯救我們,但這樣導(dǎo)致它們不能再拓展它們的框架了,因為成員變量布局被固定住了。在脆弱的實例變量(Fragile ivar)環(huán)境下,需要我們重新編譯繼承自 Apple 的類來恢復(fù)兼容。


在健壯的實例變量下,編譯器生成的實例變量布局跟以前一樣,但是當 Runtime 系統(tǒng)檢測到與父類有部分重疊時它會調(diào)整你新添加的實例變量的位移,那樣你再子類中新添加的成員變量就被保護起來了。

注意:

在健壯的實例變量下,不要使用?siof(SomeClass),而是用?class_getInstanceSize([SomeClass class])?代替;也不要使用??offsetof(SomeClass, SomeIvar),而要使用?ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))?來代替。

總結(jié)

我們讓自己的類繼承自?NSObject?不僅僅是因為基類有很多復(fù)雜的內(nèi)存分配問題,更是因為這使得我們可以享受到 Runtime 系統(tǒng)帶來的便利。

雖然平時我們很少會考慮一句簡單的調(diào)用方法,發(fā)送消息底層所做的復(fù)雜的操作,但深入理解 Runtime 系統(tǒng)的細節(jié)使得我們可以利用消息機制寫出功能更強大的代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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