Runtime詳解

參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html

簡介

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

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

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

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

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

點擊這里下載蘋果維護的開源代碼。
Runtime 的作用

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

通過 Objective-C 源代碼
通過 Foundation 框架的 NSObject 類定義的方法
通過對 Runtime 庫函數(shù)的直接調用

Objective-C 源代碼

多數(shù)情況我們只需要編寫 OC 代碼即可寡键,Runtime 系統(tǒng)自動在幕后搞定一切掀泳,還記得簡介中如果我們調用方法,編譯器會將 OC 代碼轉換成運行時代碼西轩,在運行時確定數(shù)據(jù)結構和函數(shù)员舵。
通過 Foundation 框架的 NSObject 類定義的方法

Cocoa 程序中絕大部分類都是 NSObject 類的子類,所以都繼承了 NSObject 的行為藕畔。(NSProxy 類時個例外马僻,它是個抽象超類)

一些情況下,NSObject 類僅僅定義了完成某件事情的模板注服,并沒有提供所需要的代碼巫玻。例如 -description 方法,該方法返回類內(nèi)容的字符串表示祠汇,該方法主要用來調試程序仍秤。NSObject 類并不知道子類的內(nèi)容,所以它只是返回類的名字和對象的地址可很,NSObject 的子類可以重新實現(xiàn)诗力。

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

-class方法返回對象的類苇本;
-isKindOfClass: 和 -isMemberOfClass: 方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變量);
-respondsToSelector: 檢查對象能否響應指定的消息菜拓;
-conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法瓣窄;
-methodForSelector: 返回指定方法實現(xiàn)的地址。

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

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

許多函數(shù)可以讓你使用純 C 代碼來實現(xiàn) Objc 中同樣的功能。除非是寫一些 Objc 與其他語言的橋接或是底層的 debug 工作劝贸,你在寫 Objc 代碼時一般不會用到這些 C 語言函數(shù)姨谷。對于公共接口都有哪些,后面會講到映九。我將會參考蘋果官方的 API 文檔梦湘。
一些 Runtime 的術語的數(shù)據(jù)結構

要想全面了解 Runtime 機制,我們必須先了解 Runtime 的一些術語件甥,他們都對應著數(shù)據(jù)結構捌议。
SEL

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

typedef struct objc_selector *SEL;

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

注意:
不同類中相同名字的方法所對應的 selector 是相同的热芹,由于變量的類型不同陌僵,所以不會導致它們調用方法實現(xiàn)混亂。

id

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

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

以上定義,看到 objc_object 結構體包含一個 isa 指針悦陋,根據(jù) isa 指針就可以找到對象所屬的類蜈彼。

注意:
isa 指針在代碼運行時并不總指向實例對象所屬的類型,所以不能依靠它來確定類型俺驶,要想確定類型還是需要用對象的 -class 方法幸逆。

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

typedef struct objc_class *Class;

Class 其實是指向 objc_class 結構體的指針还绘。objc_class 的數(shù)據(jù)結構如下:

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 可以看到,一個運行時類中關聯(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 不能添加屬性的原因帅霜。這里可以參考下美團技術團隊的文章:深入理解 Objective-C: Category匆背。

objc_ivar_list 結構體用來存儲成員變量的列表呼伸,而 objc_ivar 則是存儲了單個成員變量的信息;同理钝尸,objc_method_list 結構體存儲著方法數(shù)組的列表括享,而單個方法的信息則由 objc_method 結構體存儲。

值得注意的時珍促,objc_class 中也有一個 isa 指針铃辖,這說明 Objc 類本身也是一個對象。為了處理類和對象的關系猪叙,Runtime 庫創(chuàng)建了一種叫做 Meta Class(元類) 的東西娇斩,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數(shù)據(jù)穴翩。

我們所熟悉的類方法犬第,就源自于 Meta Class。我們可以理解為類方法就是類對象的實例方法芒帕。每個類僅有一個類對象歉嗓,而每個類對象僅有一個與之相關的元類。

當你發(fā)出一個類似 NSObject alloc 的消息時背蟆,實際上鉴分,這個消息被發(fā)送給了一個類對象(Class Object),這個類對象必須是一個元類的實例带膀,而這個元類同時也是一個根元類(Root Meta Class)的實例志珍。所有元類的 isa 指針最終都指向根元類。

所以當 [NSObject alloc] 這條消息發(fā)送給類對象的時候垛叨,運行時代碼 objc_msgSend() 會去它元類中查找能夠響應消息的方法實現(xiàn)碴裙,如果找到了,就會對這個類對象執(zhí)行方法調用点额。

上圖實現(xiàn)是 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 存儲了方法名寡具,方法類型和方法實現(xiàn):

方法名類型為 SEL
方法類型 method_types 是個 char 指針秤茅,存儲方法的參數(shù)類型和返回值類型
method_imp 指向了方法的實現(xiàn),本質是一個函數(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ù)指針框喳,這是由編譯器生成的。當你發(fā)起一個 ObjC 消息之后厦坛,最終它會執(zhí)行的那段代碼五垮,就是由這個函數(shù)指針指定的。而 IMP 這個函數(shù)指針就指向了這個方法的實現(xiàn)杜秸。

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

你會發(fā)現(xiàn) IMP 指向的方法與 objc_msgSend 函數(shù)類型相同,參數(shù)都包含 id 和 SEL 類型呢蛤。每個方法名都對應一個 SEL 類型的方法選擇器惶傻,而每個實例對象中的 SEL 對應的方法實現(xiàn)肯定是唯一的,通過一組 id和 SEL 參數(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 為方法調用的性能進行優(yōu)化,每當實例對象接收到一個消息時静秆,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應的方法粮揉,因為每次都要查找效率太低了,而是優(yōu)先在 Cache 中查找抚笔。

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

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

可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協(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 <Foundation/Foundation.h>

@interface Person : 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);
}

打印結果如下:

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ù)挖掘屬性的真實名稱和 @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 術語講完了,接下來就要說到消息了鸿秆。體會蘋果官方文檔中的 messages aren’t bound to method implementations until Runtime酌畜。消息直到運行時才會與方法實現(xiàn)進行綁定。

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

首先檢測這個 selector 是不是要忽略埠戳。比如 Mac OS X 開發(fā)井誉,有了垃圾回收就不理會 retain蕉扮,release 這些函數(shù)。
檢測這個 selector 的 target 是不是 nil颗圣,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會 Crash喳钟,因為運行時會被忽略掉。
如果上面兩步都通過了在岂,那么就開始查找這個類的實現(xiàn) IMP奔则,先從 cache 里查找,如果找到了就運行對應的函數(shù)去執(zhí)行相應的代碼蔽午。
如果 cache 找不到就找類的方法列表中是否有對應的方法易茬。
如果類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類為止及老。
如果還找不到抽莱,就要開始進入動態(tài)方法解析了,后面會提到骄恶。

在消息的傳遞中食铐,編譯器會根據(jù)情況在 objc_msgSend , objc_msgSend_stret 僧鲁, objc_msgSendSuper 虐呻, objc_msgSendSuper_stret 這四個方法中選擇一個調用。如果消息是傳遞給父類寞秃,那么會調用名字帶有 Super 的函數(shù)斟叼,如果消息返回值是數(shù)據(jù)結構而不是簡單值時,會調用名字帶有 stret 的函數(shù)春寿。
方法中的隱藏參數(shù)

疑問:
我們經(jīng)常用到關鍵字 self 朗涩,但是 self 是如何獲取當前方法的對象呢?

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

當 objc_msgSend 找到方法對應實現(xiàn)時,它將直接調用該方法實現(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)中訪問消息接收者對象的實例變量的途徑。

這時我們可能會想到另一個關鍵字 super 凳厢,實際上 super 關鍵字接收到消息時账胧,編譯器會創(chuàng)建一個 objc_super 結構體:

struct objc_super { id receiver; Class class; };

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

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

獲取方法地址

NSObject 類中有一個實例方法:methodForSelector意狠,你可以用它來獲取某個方法選擇器對應的 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ù)調用時疮胖,兩個隱藏參數(shù)也必須明確給出环戈,上面的例子調用了1000次函數(shù),你也可以嘗試給 target 發(fā)送1000次 setFilled: 消息會花多久澎灸。

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

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

動態(tài)方法解析

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

@dynamic propertyName;

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

當 Runtime 系統(tǒng)在 Cache 和類的方法列表(包括父類)中找不到要執(zhí)行的方法時榴徐,Runtime 會調用 resolveInstanceMethod: 或 resolveClassMethod: 來給我們一次動態(tài)添加方法實現(xiàn)的機會守问。我們需要用 class_addMethod 函數(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 方法添加了實現(xiàn)內(nèi)容,就是 dynamicMethodIMP 方法中的代碼坑资。其中 "v@:" 表示返回值和參數(shù)耗帕,這個符號表示的含義見:Type Encoding

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

消息轉發(fā)

重定向

消息轉發(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窑业,則會計入消息轉發(fā)機制(forwardInvocation:),否則將向返回的對象重新發(fā)送消息枕屉。
轉發(fā)

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

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

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

注意:參數(shù) anInvocation 是從哪來的威恼?
在 forwardInvocation: 消息發(fā)送前品姓,Runtime 系統(tǒng)會向對象發(fā)送methodSignatureForSelector: 消息,并取到返回的方法簽名用于生成 NSInvocation 對象箫措。所以重寫 forwardInvocation: 的同時也要重寫 methodSignatureForSelector: 方法腹备,否則會拋異常。

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

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

注意:
forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調用淤毛。所以,如果我們向往一個對象將一個消息轉發(fā)給其他對象時算柳,要確保這個對象不能有該消息的所對應的方法低淡。否則,forwardInvocation:將不可能被調用瞬项。

轉發(fā)和多繼承

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

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

消息轉發(fā)彌補了 Objc 不支持多繼承的性質蜂筹,也避免了因為多繼承導致單個類變得臃腫復雜。
轉發(fā)與繼承

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

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

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

回答當然是 NO麻裳, 盡管它能接受 negotiate 消息而不報錯,因為它靠轉發(fā)消息給 Diplomat 類響應消息周霉。

如果你就是想要讓別人以為 Warrior 繼承到了 Diplomat 的 negotiate 方法掂器,你得重新實現(xiàn) respondsToSelector: 和 isKindOfClass: 來加入你的轉發(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: 中也應該寫一份轉發(fā)算法俱箱。如果使用了協(xié)議国瓮,conformsToProtocol: 同樣也要加入到這一行列中。

如果一個對象想要轉發(fā)它接受的任何遠程消息,它得給出一個方法標簽來返回準確的方法描述 methodSignatureForSelector:乃摹,這個方法會最終響應被轉發(fā)的消息禁漓。從而生成一個確定的 NSInvocation 對象描述消息和消息參數(shù)。這個方法最終響應被轉發(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ū)域重疊了。只有蘋果將父類改為以前的布局才能拯救我們郭怪,但這樣導致它們不能再拓展它們的框架了支示,因為成員變量布局被固定住了。在脆弱的實例變量(Fragile ivar)環(huán)境下鄙才,需要我們重新編譯繼承自 Apple 的類來恢復兼容颂鸿。如果是健壯的實例變量的話,如下圖:

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

注意:
在健壯的實例變量下,不要使用 siof(SomeClass)位衩,而是用 class_getInstanceSize([SomeClass class]) 代替裆蒸;也不要使用 offsetof(SomeClass, SomeIvar),而要使用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 來代替糖驴。

總結

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

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

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

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

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

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

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

點擊這里下載蘋果維護的開源代碼憨愉。


Runtime 的作用

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

  1. 通過 Objective-C 源代碼
  2. 通過 Foundation 框架的 NSObject 類定義的方法
  3. 通過對 Runtime 庫函數(shù)的直接調用

Objective-C 源代碼

多數(shù)情況我們只需要編寫 OC 代碼即可烦绳,Runtime 系統(tǒng)自動在幕后搞定一切菱属,還記得簡介中如果我們調用方法思恐,編譯器會將 OC 代碼轉換成運行時代碼悯许,在運行時確定數(shù)據(jù)結構和函數(shù)铛纬。

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

Cocoa 程序中絕大部分類都是 NSObject 類的子類凫碌,所以都繼承了 NSObject 的行為鸳址。(NSProxy 類時個例外狱意,它是個抽象超類)

一些情況下式散,NSObject 類僅僅定義了完成某件事情的模板植袍,并沒有提供所需要的代碼惧眠。例如 -description 方法,該方法返回類內(nèi)容的字符串表示于个,該方法主要用來調試程序氛魁。NSObject 類并不知道子類的內(nèi)容,所以它只是返回類的名字和對象的地址厅篓,NSObject 的子類可以重新實現(xiàn)秀存。

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

  • -class方法返回對象的類或链;
  • -isKindOfClass:-isMemberOfClass: 方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變量);
  • -respondsToSelector: 檢查對象能否響應指定的消息档押;
  • -conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法澳盐;
  • -methodForSelector: 返回指定方法實現(xiàn)的地址祈纯。

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

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

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


一些 Runtime 的術語的數(shù)據(jù)結構

要想全面了解 Runtime 機制,我們必須先了解 Runtime 的一些術語匆浙,他們都對應著數(shù)據(jù)結構安寺。

SEL

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

typedef struct objc_selector *SEL;

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

注意:
不同類中相同名字的方法所對應的 selector 是相同的跋核,由于變量的類型不同岖瑰,所以不會導致它們調用方法實現(xiàn)混亂。

id

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

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

以上定義,看到 objc_object 結構體包含一個 isa 指針泊藕,根據(jù) isa 指針就可以找到對象所屬的類辅辩。

注意:
isa 指針在代碼運行時并不總指向實例對象所屬的類型,所以不能依靠它來確定類型娃圆,要想確定類型還是需要用對象的 -class 方法。

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

Class

typedef struct objc_class *Class;

Class 其實是指向 objc_class 結構體的指針。objc_class 的數(shù)據(jù)結構如下:

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 可以看到谦炬,一個運行時類中關聯(lián)了它的父類指針悦屏、類名节沦、成員變量、方法础爬、緩存以及附屬的協(xié)議甫贯。

其中 objc_ivar_listobjc_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 不能添加屬性的原因。這里可以參考下美團技術團隊的文章:深入理解 Objective-C: Category供炎。

objc_ivar_list 結構體用來存儲成員變量的列表渴逻,而 objc_ivar 則是存儲了單個成員變量的信息;同理音诫,objc_method_list 結構體存儲著方法數(shù)組的列表惨奕,而單個方法的信息則由 objc_method 結構體存儲。

值得注意的時竭钝,objc_class 中也有一個 isa 指針梨撞,這說明 Objc 類本身也是一個對象。為了處理類和對象的關系香罐,Runtime 庫創(chuàng)建了一種叫做 Meta Class(元類) 的東西卧波,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數(shù)據(jù)穴吹。

我們所熟悉的類方法幽勒,就源自于 Meta Class。我們可以理解為類方法就是類對象的實例方法港令。每個類僅有一個類對象啥容,而每個類對象僅有一個與之相關的元類。

當你發(fā)出一個類似 [NSObject alloc](類方法) 的消息時顷霹,實際上咪惠,這個消息被發(fā)送給了一個類對象(Class Object),這個類對象必須是一個元類的實例淋淀,而這個元類同時也是一個根元類(Root Meta Class)的實例遥昧。所有元類的 isa 指針最終都指向根元類。

所以當 [NSObject alloc] 這條消息發(fā)送給類對象的時候朵纷,運行時代碼 objc_msgSend() 會去它元類中查找能夠響應消息的方法實現(xiàn)炭臭,如果找到了,就會對這個類對象執(zhí)行方法調用袍辞。

image

上圖實現(xiàn)是 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 存儲了方法名吸申,方法類型和方法實現(xiàn):

  • 方法名類型為 SEL
  • 方法類型 method_types 是個 char 指針梗劫,存儲方法的參數(shù)類型和返回值類型
  • method_imp 指向了方法的實現(xiàn),本質是一個函數(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ù)指針在跳,這是由編譯器生成的。當你發(fā)起一個 ObjC 消息之后隐岛,最終它會執(zhí)行的那段代碼猫妙,就是由這個函數(shù)指針指定的。而 IMP 這個函數(shù)指針就指向了這個方法的實現(xiàn)聚凹。

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

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

而一個確定的方法也只有唯一的一組 idSEL 參數(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 為方法調用的性能進行優(yōu)化,每當實例對象接收到一個消息時旗们,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應的方法蚓哩,因為每次都要查找效率太低了,而是優(yōu)先在 Cache 中查找上渴。

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

Property

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

可以通過class_copyPropertyListprotocol_copyPropertyList 方法獲取類和協(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 <Foundation/Foundation.h>

@interface Person : 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);
    }

打印結果如下:

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ù)挖掘屬性的真實名稱和 @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_getPropertyprotocol_getProperty 通過給出屬性名在類和協(xié)議中獲得屬性的引用。


消息

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

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

image
  1. 首先檢測這個 selector 是不是要忽略屋摇。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain炮温,release 這些函數(shù)火脉。
  2. 檢測這個 selector 的 target 是不是 nil倦挂,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會 Crash白修,因為運行時會被忽略掉兵睛。
  3. 如果上面兩步都通過了,那么就開始查找這個類的實現(xiàn) IMP笛丙,先從 cache 里查找胚鸯,如果找到了就運行對應的函數(shù)去執(zhí)行相應的代碼笨鸡。
  4. 如果 cache 找不到就找類的方法列表中是否有對應的方法。
  5. 如果類的方法列表中找不到就到父類的方法列表中查找辙浑,一直找到 NSObject 類為止判呕。
  6. 如果還找不到送滞,就要開始進入動態(tài)方法解析了犁嗅,后面會提到愧哟。

在消息的傳遞中奥吩,編譯器會根據(jù)情況在 objc_msgSendobjc_msgSend_stret 霞赫, objc_msgSendSuper 肥矢, objc_msgSendSuper_stret 這四個方法中選擇一個調用甘改。如果消息是傳遞給父類十艾,那么會調用名字帶有 Super 的函數(shù)忘嫉,如果消息返回值是數(shù)據(jù)結構而不是簡單值時庆冕,會調用名字帶有 stret 的函數(shù)访递。

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

疑問:
我們經(jīng)常用到關鍵字 self ,但是 self 是如何獲取當前方法的對象呢惭载?

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

objc_msgSend 找到方法對應實現(xiàn)時田弥,它將直接調用該方法實現(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)中訪問消息接收者對象的實例變量的途徑丁逝。

這時我們可能會想到另一個關鍵字 super 梭姓,實際上 super 關鍵字接收到消息時誉尖,編譯器會創(chuàng)建一個 objc_super 結構體:

struct objc_super { id receiver; Class class; };

這個結構體指明了消息應該被傳遞給特定的父類铡恕。 receiver 仍然是 self 本身探熔,當我們想通過 [super class] 獲取父類時祭刚,編譯器其實是將指向 selfid 指針和 class 的 SEL 傳遞給了 objc_msgSendSuper 函數(shù)涡驮。只有在 NSObject 類中才能找到 class 方法捉捅,然后 class 方法底層被轉換為 object_getClass()棒口, 接著底層編譯器將代碼轉換為 objc_msgSend(objc_super->receiver, @selector(class))无牵,傳入的第一個參數(shù)是指向 selfid 指針茎毁,與調用 [self class] 相同,所以我們得到的永遠都是 self 的類型墙懂。因此你會發(fā)現(xiàn):

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

獲取方法地址

NSObject 類中有一個實例方法:methodForSelector巧勤,你可以用它來獲取某個方法選擇器對應的 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ù)調用時,兩個隱藏參數(shù)也必須明確給出柒瓣,上面的例子調用了1000次函數(shù)芙贫,你也可以嘗試給 target 發(fā)送1000次 setFilled: 消息會花多久磺平。

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

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


動態(tài)方法解析

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

@dynamic propertyName;

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

當 Runtime 系統(tǒng)在 Cache 和類的方法列表(包括父類)中找不到要執(zhí)行的方法時花墩,Runtime 會調用 resolveInstanceMethod:resolveClassMethod: 來給我們一次動態(tài)添加方法實現(xiàn)的機會冰蘑。我們需要用 class_addMethod 函數(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 方法添加了實現(xiàn)內(nèi)容允跑,就是 dynamicMethodIMP 方法中的代碼聋丝。其中 "v@:" 表示返回值和參數(shù)弱睦,這個符號表示的含義見:Type Encoding

注意:
動態(tài)方法解析會在消息轉發(fā)機制侵入前執(zhí)行况木,動態(tài)方法解析器將會首先給予提供該方法選擇器對應的 IMP 的機會火惊。如果你想讓該方法選擇器被傳送到轉發(fā)機制屹耐,就讓 resolveInstanceMethod: 方法返回 NO惶岭。


消息轉發(fā)

image

重定向

消息轉發(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,則會計入消息轉發(fā)機制(forwardInvocation:)辞居,否則將向返回的對象重新發(fā)送消息瓦灶。

轉發(fā)

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

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

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

注意:參數(shù) anInvocation 是從哪來的惕蹄?
forwardInvocation: 消息發(fā)送前卖陵,Runtime 系統(tǒng)會向對象發(fā)送methodSignatureForSelector: 消息棒旗,并取到返回的方法簽名用于生成 NSInvocation 對象铣揉。所以重寫 forwardInvocation: 的同時也要重寫 methodSignatureForSelector: 方法逛拱,否則會拋異常朽合。

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

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

注意:
forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調用饭弓。所以弟断,如果我們向往一個對象將一個消息轉發(fā)給其他對象時阀趴,要確保這個對象不能有該消息的所對應的方法棚菊。否則统求,forwardInvocation:將不可能被調用码邻。

轉發(fā)和多繼承

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

image

這使得在不同繼承體系分支下的兩個類可以實現(xiàn)“繼承”對方的方法,在上圖中 WarriorDiplomat 沒有繼承關系殴泰,但是 Warriornegotiate 消息轉發(fā)給了 Diplomat 后,就好似 DiplomatWarrior 的超類一樣浮驳。

消息轉發(fā)彌補了 Objc 不支持多繼承的性質悍汛,也避免了因為多繼承導致單個類變得臃腫復雜。

轉發(fā)與繼承

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

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

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

回答當然是 NO昆著, 盡管它能接受 negotiate 消息而不報錯,因為它靠轉發(fā)消息給 Diplomat 類響應消息糖埋。

如果你就是想要讓別人以為 Warrior 繼承到了 Diplomatnegotiate 方法宣吱,你得重新實現(xiàn) respondsToSelector:isKindOfClass: 來加入你的轉發(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: 中也應該寫一份轉發(fā)算法瞳别。如果使用了協(xié)議征候,conformsToProtocol: 同樣也要加入到這一行列中。

如果一個對象想要轉發(fā)它接受的任何遠程消息祟敛,它得給出一個方法標簽來返回準確的方法描述 methodSignatureForSelector:疤坝,這個方法會最終響應被轉發(fā)的消息。從而生成一個確定的 NSInvocation 對象描述消息和消息參數(shù)馆铁。這個方法最終響應被轉發(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)生位移:

image

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

image

我們自定義的類的區(qū)域和父類的區(qū)域重疊了例驹。只有蘋果將父類改為以前的布局才能拯救我們捐韩,但這樣導致它們不能再拓展它們的框架了,因為成員變量布局被固定住了鹃锈。在脆弱的實例變量(Fragile ivar)環(huán)境下荤胁,需要我們重新編譯繼承自 Apple 的類來恢復兼容。如果是健壯的實例變量的話屎债,如下圖:

[圖片上傳失敗...(image-8cf21-1510822320510)]

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

注意:
在健壯的實例變量下秸苗,不要使用 siof(SomeClass)召娜,而是用 class_getInstanceSize([SomeClass class]) 代替;也不要使用 offsetof(SomeClass, SomeIvar)惊楼,而要使用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 來代替玖瘸。


總結

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

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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔑匣,一起剝皮案震驚了整個濱河市劣欢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裁良,老刑警劉巖凿将,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異价脾,居然都是意外死亡牧抵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門侨把,熙熙樓的掌柜王于貴愁眉苦臉地迎上來犀变,“玉大人,你說我怎么就攤上這事秋柄』裰Γ” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵华匾,是天一觀的道長映琳。 經(jīng)常有香客問我屡律,道長祷肯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任鼓黔,我火速辦了婚禮旭旭,結果婚禮上谎脯,老公的妹妹穿的比我還像新娘。我一直安慰自己持寄,他們只是感情好源梭,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稍味,像睡著了一般废麻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上模庐,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天烛愧,我揣著相機與錄音,去河邊找鬼。 笑死怜姿,一個胖子當著我的面吹牛慎冤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沧卢,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蚁堤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了但狭?” 一聲冷哼從身側響起披诗,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎熟空,沒想到半個月后藤巢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡息罗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年掂咒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迈喉。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡绍刮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挨摸,到底是詐尸還是另有隱情孩革,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布得运,位于F島的核電站膝蜈,受9級特大地震影響,放射性物質發(fā)生泄漏熔掺。R本人自食惡果不足惜饱搏,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望置逻。 院中可真熱鬧推沸,春花似錦、人聲如沸券坞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恨锚。三九已至宇驾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間猴伶,已是汗流浹背课舍。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工菌瘫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人布卡。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像雇盖,于是被迫代替她去往敵國和親忿等。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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

  • 轉至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉崔挖,最新修改于: 十二月 23, 2016 轉至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,681評論 0 9
  • 簡介 Runtime 又叫運行時贸街,是一套底層的 C 語言 API,其為 iOS 內(nèi)部的核心之一狸相,我們平時編寫的 O...
    專業(yè)男神經(jīng)閱讀 902評論 0 2
  • 轉發(fā)自一個低調的iOS開發(fā) 簡介 Runtime 又叫運行時薛匪,是一套底層的 C 語言 API,其為 iOS 內(nèi)部的...
    葉子揚閱讀 607評論 0 2
  • 運行時是iOS中一個很重要的概念脓鹃,iOS運行過程中都會被轉化為runtime的C代碼執(zhí)行逸尖。例如[target do...
    蘿卜醬紫閱讀 387評論 0 3
  • 大學時期我一直保持著記賬的習慣,盡管很多時候都是重復的吃飯龄章、交通吃谣、日用品等費用,不過也沒有因為無聊而中斷過做裙。每學期...
    白日夢想家_09閱讀 1,087評論 7 16