詳解Runtime運(yùn)行時機(jī)制

簡介

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

[receiver message];

// 底層運(yùn)行時會被編譯器轉(zhuǎn)化為:

objc_msgSend(receiver, selector)

// 如果其還有參數(shù)比如:

[receiver message:(id)arg...];

// 底層運(yùn)行時會被編譯器轉(zhuǎn)化為:

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

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

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

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

點(diǎn)擊這里下載蘋果維護(hù)的開源代碼熊痴。

Runtime 的作用

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

通過 Objective-C 源代碼

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

通過對 Runtime 庫函數(shù)的直接調(diào)用

Objective-C 源代碼

多數(shù)情況我們只需要編寫 OC 代碼即可,Runtime 系統(tǒng)自動在幕后搞定一切聂宾,還記得簡介中如果我們調(diào)用方法果善,編譯器會將 OC 代碼轉(zhuǎn)換成運(yùn)行時代碼,在運(yùn)行時確定數(shù)據(jù)結(jié)構(gòu)和函數(shù)系谐。

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

Cocoa 程序中絕大部分類都是 NSObject 類的子類巾陕,所以都繼承了 NSObject 的行為。(NSProxy ?類是個例外纪他,它是個抽象超類)

一些情況下鄙煤,NSObject 類僅僅定義了完成某件事情的模板,并沒有提供所需要的代碼茶袒。例如-description方法梯刚,該方法返回類內(nèi)容的字符串表示,該方法主要用來調(diào)試程序薪寓。NSObject 類并不知道子類的內(nèi)容亡资,所以它只是返回類的名字和對象的地址,NSObject 的子類可以重新實(shí)現(xiàn)向叉。

還有一些 NSObject 的方法可以從 Runtime 系統(tǒng)中獲取信息锥腻,允許對象進(jìn)行自我檢查。例如:

-class方法返回對象的類母谎;

-isKindOfClass:和-isMemberOfClass:方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當(dāng)前類的成員變量)瘦黑;

-respondsToSelector:檢查對象能否響應(yīng)指定的消息;

-conformsToProtocol:檢查對象是否實(shí)現(xiàn)了指定協(xié)議類的方法奇唤;

-methodForSelector:返回指定方法實(shí)現(xiàn)的地址幸斥。

通過對 Runtime 庫函數(shù)的直接調(diào)用

Runtime 系統(tǒng)是具有公共接口的動態(tài)共享庫。頭文件存放于/usr/include/objc目錄下咬扇,這意味著我們使用時只需要引入objc/Runtime.h頭文件即可甲葬。

許多函數(shù)可以讓你使用純 C 代碼來實(shí)現(xiàn) Objc 中同樣的功能。除非是寫一些 Objc 與其他語言的橋接或是底層的 debug 工作冗栗,你在寫 Objc 代碼時一般不會用到這些 C 語言函數(shù)演顾。對于公共接口都有哪些,后面會講到隅居。我將會參考蘋果官方的 API 文檔钠至。

一些 Runtime 術(shù)語對應(yīng)的數(shù)據(jù)結(jié)構(gòu)

要想全面了解 Runtime 機(jī)制,我們必須先了解 Runtime 的一些術(shù)語胎源,他們都對應(yīng)著數(shù)據(jù)結(jié)構(gòu)棉钧。

SEL

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

typedef struct objc_selector *SEL;

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

注意:

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

id

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

typedef struct objc_object *id;

struct objc_object { Class isa; };

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

注意:

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

PS:KVO 的實(shí)現(xiàn)機(jī)理就是將被觀察對象的 isa 指針指向一個中間類而不是真實(shí)類型,詳見:KVO章節(jié)层释。

Class

typedef struct objc_class *Class;

Class其實(shí)是指向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;

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;

struct?objc_cache?*cache? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?OBJC2_UNAVAILABLE;

struct?objc_protocol_list?*protocols? ? ? ? ? ? ? ? ? ? ?OBJC2_UNAVAILABLE;

#endif

}?OBJC2_UNAVAILABLE;

從objc_class可以看到,一個運(yùn)行時類中關(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 實(shí)現(xiàn)的原理磅轻,同樣解釋了 Category 不能添加屬性的原因珍逸。這里可以參考下美團(tuán)技術(shù)團(tuán)隊(duì)的文章:深入理解 Objective-C: 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当凡。我們可以理解為類方法就是類對象的實(shí)例方法。每個類僅有一個類對象纠俭,而每個類對象僅有一個與之相關(guān)的元類沿量。

當(dāng)你發(fā)出一個類似[NSObject alloc](類方法)的消息時,實(shí)際上冤荆,這個消息被發(fā)送給了一個類對象(Class Object)朴则,這個類對象必須是一個元類的實(shí)例,而這個元類同時也是一個根元類(Root Meta Class)的實(shí)例钓简。所有元類的 isa 指針最終都指向根元類乌妒。

所以當(dāng)[NSObject alloc]這條消息發(fā)送給類對象的時候,運(yùn)行時代碼objc_msgSend()會去它元類中查找能夠響應(yīng)消息的方法實(shí)現(xiàn)外邓,如果找到了撤蚊,就會對這個類對象執(zhí)行方法調(diào)用。

上圖實(shí)線是super_class指針损话,虛線是isa指針侦啸。而根元類的父類是NSObject,isa指向了自己丧枪。而NSObject沒有父類光涂。

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

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;

}

objc_method存儲了方法名恋博,方法類型和方法實(shí)現(xiàn):

方法名類型為SEL

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

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

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

}

其中ivar_offset是基地址偏移字節(jié)

IMP

IMP在objc.h中的定義是:

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

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

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

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

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

Cache

Cache 定義如下:

typedef struct objc_cache *Cache

struct objc_cache {

unsigned int mask /* total = mask + 1 */? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

unsigned int occupied? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

Method buckets[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

};

Cache 為方法調(diào)用的性能進(jìn)行優(yōu)化,每當(dāng)實(shí)例對象接收到一個消息時异剥,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應(yīng)的方法瑟由,因?yàn)槊看味家檎倚侍土耍莾?yōu)先在 Cache 中查找冤寿。

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

Property

typedef struct objc_property *Property;

typedef struct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList和protocol_copyPropertyList函數(shù)獲取類和協(xié)議中的屬性

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意:

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

#import

@interface Person : NSObject

/** 姓名 */

@property (strong, nonatomic) NSString *name;

/** age */

@property (assign, nonatomic) int age;

/** weight */

@property (assign, nonatomic) double weight;

@end

以上是一個 Person 類蚪腋,有3個屬性。讓我們用上述方法獲取類的運(yùn)行時屬性姨蟋。

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-10?11:27:28.473?test[2321:451525]?3

2014-11-10?11:27:28.473?test[2321:451525]?name--------T@"NSString",&,N,V_name

2014-11-10?11:27:28.473?test[2321:451525]?age--------Ti,N,V_age

2014-11-10?11:27:28.474?test[2321:451525]?weight--------Td,N,V_weight

property_getName用來查找屬性的名稱辣吃,返回 c 字符串。property_getAttributes函數(shù)挖掘?qū)傩缘恼鎸?shí)名稱和@encode類型芬探,返回 c 字符串神得。

objc_property_t class_getProperty(Class cls, const char *name)

objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

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

消息

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

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

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

檢測這個selector的 target 是不是nil,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會 Crash丽啡,因?yàn)檫\(yùn)行時會被忽略掉谋右。

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

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

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

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

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

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

疑問:

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

其實(shí),這也是 Runtime 系統(tǒng)的作用猜丹,self是在方法運(yùn)行時被動態(tài)傳入的芝加。

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

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

方法選擇器(_cmd指向的內(nèi)容蝌麸,當(dāng)前方法的 SEL 指針)

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

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

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

struct objc_super { id receiver; Class class; };

這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類历葛。receiver仍然是self本身正塌,當(dāng)我們想通過[super class]獲取父類時嘀略,編譯器其實(shí)是將指向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]相同讼育,所以我們得到的永遠(yuǎn)都是self的類型。因此你會發(fā)現(xiàn):

// 這句話并不能獲取父類的類型稠集,只能獲取當(dāng)前類的類型名

NSLog(@"%@", NSStringFromClass([super class]));

獲取方法地址

NSObject類中有一個實(shí)例方法: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);

當(dāng)方法被當(dāng)做函數(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)提供一個方法實(shí)現(xiàn)确买。如果我們使用關(guān)鍵字@dynamic在類的實(shí)現(xiàn)文件中修飾一個屬性斤讥,表明我們會為這個屬性動態(tài)提供存取方法,編譯器不會再默認(rèn)為我們生成這個屬性的 setter 和 getter 方法了湾趾,需要我們自己提供芭商。

@dynamic propertyName;

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

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

void dynamicMethodIMP(id self, SEL _cmd) {

// implementation ....

}

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)aSEL

{

if (aSEL == @selector(resolveThisMethodDynamically)) {

class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");

return YES;

}

return [super resolveInstanceMethod:aSEL];

}

@end

上面的例子為resolveThisMethodDynamically方法添加了實(shí)現(xiàn)內(nèi)容,就是dynamicMethodIMP方法中的代碼蛉艾。其中"v@:"表示返回值和參數(shù)钳踊,這個符號表示的含義見:Type Encoding

注意:

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

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

重定向

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

- (id)forwardingTargetForSelector:(SEL)aSelector

{

if(aSelector == @selector(mysteriousMethod:)){

return alternateObject;

}

return [super forwardingTargetForSelector:aSelector];

}

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

轉(zhuǎn)發(fā)

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

- (void)forwardInvocation:(NSInvocation *)anInvocation

{

if ([someOtherObject respondsToSelector:

[anInvocation selector]])

[anInvocation invokeWithTarget:someOtherObject];

else

[super forwardInvocation:anInvocation];

}

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

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

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

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

forwardInvocation:方法就是一個不能識別消息的分發(fā)中心,將這些不能識別的消息轉(zhuǎn)發(fā)給不同的接收對象登刺,或者轉(zhuǎn)發(fā)給同一個對象籽腕,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息纸俭,因此沒有響應(yīng)也不會報(bào)錯皇耗。這一切都取決于方法的具體實(shí)現(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ā)出去聋庵,就好像它把另一個對象中的方法接過來或者“繼承”過來一樣膘融。

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

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

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

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

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

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

...

回答當(dāng)然是NO铺韧, 盡管它能接受negotiate消息而不報(bào)錯,因?yàn)樗哭D(zhuǎn)發(fā)消息給Diplomat類響應(yīng)消息缓淹。

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

- (BOOL)respondsToSelector:(SEL)aSelector

{

if ( [super respondsToSelector:aSelector] )

return YES;

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.? */

}

return NO;

}

除了respondsToSelector:和isKindOfClass:之外,instancesRespondToSelector:中也應(yīng)該寫一份轉(zhuǎn)發(fā)算法讯壶。如果使用了協(xié)議料仗,conformsToProtocol:同樣也要加入到這一行列中。

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

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector

{

NSMethodSignature* signature = [super methodSignatureForSelector:selector];

if (!signature) {

signature = [surrogate methodSignatureForSelector:selector];

}

return signature;

}

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

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

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

我們自定義的類的區(qū)域和父類的區(qū)域重疊了。只有蘋果將父類改為以前的布局才能拯救我們芒率,但這樣導(dǎo)致它們不能再拓展它們的框架了囤耳,因?yàn)槌蓡T變量布局被固定住了泰偿。在脆弱的實(shí)例變量(Fragile ivar)環(huán)境下敞峭,需要我們重新編譯繼承自 Apple 的類來恢復(fù)兼容靖苇。如果是健壯的實(shí)例變量的話球涛,如下圖:

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

注意:

在健壯的實(shí)例變量下材彪,不要使用siof(SomeClass)观挎,而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar)段化,而要使用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))來代替嘁捷。

總結(jié)

我們讓自己的類繼承自NSObject不僅僅是因?yàn)榛愑泻芏鄰?fù)雜的內(nèi)存分配問題,更是因?yàn)檫@使得我們可以享受到 Runtime 系統(tǒng)帶來的便利显熏。

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

本文參考:

本文參考Objective-C 2.0運(yùn)行時系統(tǒng)編程指南

蘋果官方 Runtime 文檔

Objective-C Runtime

Understanding the Objective-C Runtime

文/Ammar(簡書作者)

原文鏈接:http://www.jianshu.com/p/1e06bfee99d0

著作權(quán)歸作者所有缓升,轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),并標(biāo)注“簡書作者”蕴轨。

最后編輯于
?著作權(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
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了一罩。 大學(xué)時的朋友給我發(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)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉碉纳,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評論 0 9
  • 原文出處:南峰子的技術(shù)博客 Objective-C語言是一門動態(tài)語言,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了...
    _燴面_閱讀 1,215評論 1 5
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,129評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言馏艾,那么這個「動態(tài)」表現(xiàn)在哪呢劳曹?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,172評論 0 7
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 727評論 0 2