參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html
簡介
Runtime 又叫運(yùn)行時(shí),是一套底層的 C 語言 API,其為 iOS 內(nèi)部的核心之一,我們平時(shí)編寫的 OC 代碼,底層都是基于它來實(shí)現(xiàn)的。比如:
[receiver message];
// 底層運(yùn)行時(shí)會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
// 如果其還有參數(shù)比如:
[receiver message:(id)arg...];
// 底層運(yùn)行時(shí)會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
以上你可能看不出它的價(jià)值,但是我們需要了解的是 Objective-C 是一門動(dòng)態(tài)語言,它會將一些工作放在代碼運(yùn)行時(shí)才處理而并非編譯時(shí)。也就是說,有很多類和成員變量在我們編譯的時(shí)是不知道的,而在運(yùn)行時(shí),我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運(yùn)行。
因此,編譯器是不夠的,我們還需要一個(gè)運(yùn)行時(shí)系統(tǒng)(Runtime system)來處理編譯后的代碼。
Runtime 基本是用 C 和匯編寫的,由此可見蘋果為了動(dòng)態(tài)系統(tǒng)的高效而做出的努力。蘋果和 GNU 各自維護(hù)一個(gè)開源的 Runtime 版本,這兩個(gè)版本之間都在努力保持一致。
點(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)自動(dòng)在幕后搞定一切,還記得簡介中如果我們調(diào)用方法,編譯器會將 OC 代碼轉(zhuǎn)換成運(yùn)行時(shí)代碼,在運(yùn)行時(shí)確定數(shù)據(jù)結(jié)構(gòu)和函數(shù)。
通過 Foundation 框架的 NSObject 類定義的方法
Cocoa 程序中絕大部分類都是 NSObject 類的子類,所以都繼承了 NSObject 的行為。(NSProxy 類時(shí)個(gè)例外,它是個(gè)抽象超類)
一些情況下,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)是具有公共接口的動(dòng)態(tài)共享庫。頭文件存放于/usr/include/objc目錄下,這意味著我們使用時(shí)只需要引入objc/Runtime.h頭文件即可。
許多函數(shù)可以讓你使用純 C 代碼來實(shí)現(xiàn) Objc 中同樣的功能。除非是寫一些 Objc 與其他語言的橋接或是底層的 debug 工作,你在寫 Objc 代碼時(shí)一般不會用到這些 C 語言函數(shù)。對于公共接口都有哪些,后面會講到。我將會參考蘋果官方的 API 文檔。
一些 Runtime 的術(shù)語的數(shù)據(jù)結(jié)構(gòu)
要想全面了解 Runtime 機(jī)制,我們必須先了解 Runtime 的一些術(shù)語,他們都對應(yīng)著數(shù)據(jù)結(jié)構(gòu)。
SEL
它是selector在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實(shí)作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個(gè)方法。selector 對方法名進(jìn)行包裝,以便找到對應(yīng)的方法實(shí)現(xiàn)。它的數(shù)據(jù)結(jié)構(gòu)是:
typedef struct objc_selector *SEL;
我們可以看出它是個(gè)映射到方法的 C 字符串,你可以通過 Objc 編譯器器命令@selector() 或者 Runtime 系統(tǒng)的 sel_registerName 函數(shù)來獲取一個(gè) SEL 類型的方法選擇器。
注意:
不同類中相同名字的方法所對應(yīng)的 selector 是相同的,由于變量的類型不同,所以不會導(dǎo)致它們調(diào)用方法實(shí)現(xiàn)混亂。
id
id 是一個(gè)參數(shù)類型,它是指向某個(gè)類的實(shí)例的指針。定義如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
以上定義,看到 objc_object 結(jié)構(gòu)體包含一個(gè) isa 指針,根據(jù) isa 指針就可以找到對象所屬的類。
注意:
isa 指針在代碼運(yùn)行時(shí)并不總指向?qū)嵗龑ο笏鶎俚念愋停圆荒芤揽克鼇泶_定類型,要想確定類型還是需要用對象的 -class 方法。
PS:KVO 的實(shí)現(xiàn)機(jī)理就是將被觀察對象的 isa 指針指向一個(gè)中間類而不是真實(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 可以看到,一個(gè)運(yùn)行時(shí)類中關(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;
}
由此可見,我們可以動(dòng)態(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 則是存儲了單個(gè)成員變量的信息;同理,objc_method_list 結(jié)構(gòu)體存儲著方法數(shù)組的列表,而單個(gè)方法的信息則由 objc_method 結(jié)構(gòu)體存儲。
值得注意的時(shí),objc_class 中也有一個(gè) isa 指針,這說明 Objc 類本身也是一個(gè)對象。為了處理類和對象的關(guān)系,Runtime 庫創(chuàng)建了一種叫做 Meta Class(元類) 的東西,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數(shù)據(jù)。
我們所熟悉的類方法,就源自于 Meta Class。我們可以理解為類方法就是類對象的實(shí)例方法。每個(gè)類僅有一個(gè)類對象,而每個(gè)類對象僅有一個(gè)與之相關(guān)的元類。
當(dāng)你發(fā)出一個(gè)類似 NSObject alloc 的消息時(shí),實(shí)際上,這個(gè)消息被發(fā)送給了一個(gè)類對象(Class Object),這個(gè)類對象必須是一個(gè)元類的實(shí)例,而這個(gè)元類同時(shí)也是一個(gè)根元類(Root Meta Class)的實(shí)例。所有元類的 isa 指針最終都指向根元類。
所以當(dāng) [NSObject alloc] 這條消息發(fā)送給類對象的時(shí)候,運(yùn)行時(shí)代碼 objc_msgSend() 會去它元類中查找能夠響應(yīng)消息的方法實(shí)現(xiàn),如果找到了,就會對這個(gè)類對象執(zhí)行方法調(diào)用。
上圖實(shí)現(xiàn)是 super_class 指針,虛線時(shí) isa 指針。而根元類的父類是 NSObject,isa指向了自己。而 NSObject 沒有父類。
最后 objc_class 中還有一個(gè) objc_cache ,緩存,它的作用很重要,后面會提到。
Method
Method 代表類中某個(gè)方法的類型
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 是個(gè) char 指針,存儲方法的參數(shù)類型和返回值類型
method_imp 指向了方法的實(shí)現(xiàn),本質(zhì)是一個(gè)函數(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, ...);
它就是一個(gè)函數(shù)指針,這是由編譯器生成的。當(dāng)你發(fā)起一個(gè) ObjC 消息之后,最終它會執(zhí)行的那段代碼,就是由這個(gè)函數(shù)指針指定的。而 IMP 這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)。
如果得到了執(zhí)行某個(gè)實(shí)例某個(gè)方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面 Cache 中會提到。
你會發(fā)現(xiàn) IMP 指向的方法與 objc_msgSend 函數(shù)類型相同,參數(shù)都包含 id 和 SEL 類型。每個(gè)方法名都對應(yīng)一個(gè) SEL 類型的方法選擇器,而每個(gè)實(shí)例對象中的 SEL 對應(yīng)的方法實(shí)現(xiàn)肯定是唯一的,通過一組 id和 SEL 參數(shù)就能確定唯一的方法實(shí)現(xiàn)地址。
而一個(gè)確定的方法也只有唯一的一組 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í)例對象接收到一個(gè)消息時(shí),它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應(yīng)的方法,因?yàn)槊看味家檎倚侍土耍莾?yōu)先在 Cache 中查找。
Runtime 系統(tǒng)會把被調(diào)用的方法存到 Cache 中,如果一個(gè)方法被調(diào)用,那么它有可能今后還會被調(diào)用,下次查找的時(shí)候就會效率更高。就像計(jì)算機(jī)組成原理中 CPU 繞過主存先訪問 Cache 一樣。
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個(gè)更常用
可以通過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)
注意:
返回的是屬性列表,列表中每個(gè)元素都是一個(gè) 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
以上是一個(gè) Person 類,有3個(gè)屬性。讓我們用上述方法獲取類的運(yùn)行時(shí)屬性。
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í)才會與方法實(shí)現(xiàn)進(jìn)行綁定。
這里要清楚一點(diǎn),objc_msgSend 方法看清來好像返回了數(shù)據(jù),其實(shí)objc_msgSend 從不返回?cái)?shù)據(jù),而是你的方法在運(yùn)行時(shí)實(shí)現(xiàn)被調(diào)用后才會返回?cái)?shù)據(jù)。下面詳細(xì)敘述消息發(fā)送的步驟(如下圖):
首先檢測這個(gè) selector 是不是要忽略。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù)。
檢測這個(gè) selector 的 target 是不是 nil,Objc 允許我們對一個(gè) nil 對象執(zhí)行任何方法不會 Crash,因?yàn)檫\(yùn)行時(shí)會被忽略掉。
如果上面兩步都通過了,那么就開始查找這個(gè)類的實(shí)現(xiàn) IMP,先從 cache 里查找,如果找到了就運(yùn)行對應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼。
如果 cache 找不到就找類的方法列表中是否有對應(yīng)的方法。
如果類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類為止。
如果還找不到,就要開始進(jìn)入動(dòng)態(tài)方法解析了,后面會提到。
在消息的傳遞中,編譯器會根據(jù)情況在 objc_msgSend , objc_msgSend_stret , objc_msgSendSuper , objc_msgSendSuper_stret 這四個(gè)方法中選擇一個(gè)調(diào)用。如果消息是傳遞給父類,那么會調(diào)用名字帶有 Super 的函數(shù),如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時(shí),會調(diào)用名字帶有 stret 的函數(shù)。
方法中的隱藏參數(shù)
疑問:
我們經(jīng)常用到關(guān)鍵字 self ,但是 self 是如何獲取當(dāng)前方法的對象呢?
其實(shí),這也是 Runtime 系統(tǒng)的作用,self 實(shí)在方法運(yùn)行時(shí)被動(dòng)態(tài)傳入的。
當(dāng) objc_msgSend 找到方法對應(yīng)實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn),并將消息中所有參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí),它還將傳遞兩個(gè)隱藏參數(shù):
接受消息的對象(self 所指向的內(nèi)容,當(dāng)前方法的對象指針)
方法選擇器(_cmd 指向的內(nèi)容,當(dāng)前方法的 SEL 指針)
因?yàn)樵谠创a方法的定義中,我們并沒有發(fā)現(xiàn)這兩個(gè)參數(shù)的聲明。它們時(shí)在代碼被編譯時(shí)被插入方法實(shí)現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?/p>
這兩個(gè)參數(shù)中, self更實(shí)用。它是在方法實(shí)現(xiàn)中訪問消息接收者對象的實(shí)例變量的途徑。
這時(shí)我們可能會想到另一個(gè)關(guān)鍵字 super ,實(shí)際上 super 關(guān)鍵字接收到消息時(shí),編譯器會創(chuàng)建一個(gè) objc_super 結(jié)構(gòu)體:
struct objc_super { id receiver; Class class; };
這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類。 receiver 仍然是 self 本身,當(dāng)我們想通過 [super class] 獲取父類時(shí),編譯器其實(shí)是將指向 self 的 id 指針和 class 的 SEL 傳遞給了 objc_msgSendSuper 函數(shù)。只有在 NSObject 類中才能找到 class 方法,然后 class 方法底層被轉(zhuǎn)換為 object_getClass(), 接著底層編譯器將代碼轉(zhuǎn)換為 objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個(gè)參數(shù)是指向 self 的 id 指針,與調(diào)用 [self class] 相同,所以我們得到的永遠(yuǎn)都是 self 的類型。因此你會發(fā)現(xiàn):
// 這句話并不能獲取父類的類型,只能獲取當(dāng)前類的類型名
NSLog(@"%@", NSStringFromClass([super class]));
獲取方法地址
NSObject 類中有一個(gè)實(shí)例方法:methodForSelector,你可以用它來獲取某個(gè)方法選擇器對應(yīng)的 IMP ,舉個(gè)例子:
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í),兩個(gè)隱藏參數(shù)也必須明確給出,上面的例子調(diào)用了1000次函數(shù),你也可以嘗試給 target 發(fā)送1000次 setFilled: 消息會花多久。
雖然可以更高效的調(diào)用方法,但是這種做法很少用,除非時(shí)需要持續(xù)大量重復(fù)調(diào)用某個(gè)方法的情況,才會選擇使用以免消息發(fā)送泛濫。
注意:
methodForSelector:方法是由 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性
動(dòng)態(tài)方法解析
你可以動(dòng)態(tài)提供一個(gè)方法實(shí)現(xiàn)。如果我們使用關(guān)鍵字 @dynamic 在類的實(shí)現(xiàn)文件中修飾一個(gè)屬性,表明我們會為這個(gè)屬性動(dòng)態(tài)提供存取方法,編譯器不會再默認(rèn)為我們生成這個(gè)屬性的 setter 和 getter 方法了,需要我們自己提供。
@dynamic propertyName;
這時(shí),我們可以通過分別重載 resolveInstanceMethod: 和 resolveClassMethod: 方法添加實(shí)例方法實(shí)現(xiàn)和類方法實(shí)現(xiàn)。
當(dāng) Runtime 系統(tǒng)在 Cache 和類的方法列表(包括父類)中找不到要執(zhí)行的方法時(shí),Runtime 會調(diào)用 resolveInstanceMethod: 或 resolveClassMethod: 來給我們一次動(dòng)態(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ù),這個(gè)符號表示的含義見:Type Encoding
注意:
動(dòng)態(tài)方法解析會在消息轉(zhuǎn)發(fā)機(jī)制侵入前執(zhí)行,動(dòng)態(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ì)入消息轉(zhuǎn)發(fā)機(jī)制(forwardInvocation:),否則將向返回的對象重新發(fā)送消息。
轉(zhuǎn)發(fā)
當(dāng)動(dòng)態(tài)方法解析不做處理返回 NO 時(shí),則會觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制。這時(shí) forwardInvocation: 方法會被執(zhí)行,我們可以重寫這個(gè)方法來自定義我們的轉(zhuǎn)發(fā)邏輯:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
唯一參數(shù)是個(gè) NSInvocation 類型的對象,該對象封裝了原始的消息和消息的參數(shù)。我們可以實(shí)現(xiàn) forwardInvocation: 方法來對不能處理的消息做一些處理。也可以將消息轉(zhuǎn)發(fā)給其他對象處理,而不拋出錯(cuò)誤。
注意:參數(shù) anInvocation 是從哪來的?
在 forwardInvocation: 消息發(fā)送前,Runtime 系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector: 消息,并取到返回的方法簽名用于生成 NSInvocation 對象。所以重寫 forwardInvocation: 的同時(shí)也要重寫 methodSignatureForSelector: 方法,否則會拋異常。
當(dāng)一個(gè)對象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法相應(yīng)某消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過 forwardInvocation: 消息通知該對象。每個(gè)對象都繼承了 forwardInvocation: 方法。但是, NSObject 中的方法實(shí)現(xiàn)只是簡單的調(diào)用了 doesNotRecognizeSelector:。通過實(shí)現(xiàn)自己的 forwardInvocation: 方法,我們可以將消息轉(zhuǎn)發(fā)給其他對象。
forwardInvocation: 方法就是一個(gè)不能識別消息的分發(fā)中心,將這些不能識別的消息轉(zhuǎn)發(fā)給不同的接收對象,或者轉(zhuǎn)發(fā)給同一個(gè)對象,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息,因此沒有響應(yīng)也不會報(bào)錯(cuò)。這一切都取決于方法的具體實(shí)現(xiàn)。
注意:
forwardInvocation:方法只有在消息接收對象中無法正常響應(yīng)消息時(shí)才會被調(diào)用。所以,如果我們向往一個(gè)對象將一個(gè)消息轉(zhuǎn)發(fā)給其他對象時(shí),要確保這個(gè)對象不能有該消息的所對應(yīng)的方法。否則,forwardInvocation:將不可能被調(diào)用。
轉(zhuǎn)發(fā)和多繼承
轉(zhuǎn)發(fā)和繼承相似,可用于為 Objc 編程添加一些多繼承的效果。就像下圖那樣,一個(gè)對象把消息轉(zhuǎn)發(fā)出去,就好像它把另一個(gè)對象中的方法接過來或者“繼承”過來一樣。
這使得在不同繼承體系分支下的兩個(gè)類可以實(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)致單個(gè)類變得臃腫復(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)錯(cuò),因?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: 同樣也要加入到這一行列中。
如果一個(gè)對象想要轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息,它得給出一個(gè)方法標(biāo)簽來返回準(zhǔn)確的方法描述 methodSignatureForSelector:,這個(gè)方法會最終響應(yīng)被轉(zhuǎn)發(fā)的消息。從而生成一個(gè)確定的 NSInvocation 對象描述消息和消息參數(shù)。這個(gè)方法最終響應(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)一個(gè)類被編譯時(shí),實(shí)例變量的內(nèi)存布局就形成了,它表明訪問類的實(shí)例變量的位置。實(shí)例變量一次根據(jù)自己所占空間而產(chǎn)生位移:
上圖左是 NSObject 類的實(shí)例變量布局。右邊是我們寫的類的布局。這樣子有一個(gè)很大的缺陷,就是缺乏拓展性。哪天蘋果更新了 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)檢測到與父類有部分重疊時(shí)它會調(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)帶來的便利。
雖然平時(shí)我們很少會考慮一句簡單的調(diào)用方法,發(fā)送消息底層所做的復(fù)雜的操作,但深入理解 Runtime 系統(tǒng)的細(xì)節(jié)使得我們可以利用消息機(jī)制寫出功能更強(qiáng)大的代碼。## 簡介
Runtime 又叫運(yùn)行時(shí),是一套底層的 C 語言 API,其為 iOS 內(nèi)部的核心之一,我們平時(shí)編寫的 OC 代碼,底層都是基于它來實(shí)現(xiàn)的。比如:
[receiver message];
// 底層運(yùn)行時(shí)會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
// 如果其還有參數(shù)比如:
[receiver message:(id)arg...];
// 底層運(yùn)行時(shí)會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
以上你可能看不出它的價(jià)值,但是我們需要了解的是 Objective-C 是一門動(dòng)態(tài)語言,它會將一些工作放在代碼運(yùn)行時(shí)才處理而并非編譯時(shí)。也就是說,有很多類和成員變量在我們編譯的時(shí)是不知道的,而在運(yùn)行時(shí),我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運(yùn)行。
因此,編譯器是不夠的,我們還需要一個(gè)運(yùn)行時(shí)系統(tǒng)(Runtime system)來處理編譯后的代碼。
Runtime 基本是用 C 和匯編寫的,由此可見蘋果為了動(dòng)態(tài)系統(tǒng)的高效而做出的努力。蘋果和 GNU 各自維護(hù)一個(gè)開源的 Runtime 版本,這兩個(gè)版本之間都在努力保持一致。
點(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)自動(dòng)在幕后搞定一切,還記得簡介中如果我們調(diào)用方法,編譯器會將 OC 代碼轉(zhuǎn)換成運(yùn)行時(shí)代碼,在運(yùn)行時(shí)確定數(shù)據(jù)結(jié)構(gòu)和函數(shù)。
通過 Foundation 框架的 NSObject 類定義的方法
Cocoa 程序中絕大部分類都是 NSObject 類的子類,所以都繼承了 NSObject 的行為。(NSProxy 類時(shí)個(gè)例外,它是個(gè)抽象超類)
一些情況下,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)是具有公共接口的動(dòng)態(tài)共享庫。頭文件存放于/usr/include/objc目錄下,這意味著我們使用時(shí)只需要引入objc/Runtime.h
頭文件即可。
許多函數(shù)可以讓你使用純 C 代碼來實(shí)現(xiàn) Objc 中同樣的功能。除非是寫一些 Objc 與其他語言的橋接或是底層的 debug 工作,你在寫 Objc 代碼時(shí)一般不會用到這些 C 語言函數(shù)。對于公共接口都有哪些,后面會講到。我將會參考蘋果官方的 API 文檔。
一些 Runtime 的術(shù)語的數(shù)據(jù)結(jié)構(gòu)
要想全面了解 Runtime 機(jī)制,我們必須先了解 Runtime 的一些術(shù)語,他們都對應(yīng)著數(shù)據(jù)結(jié)構(gòu)。
SEL
它是selector
在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實(shí)作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個(gè)方法。selector 對方法名進(jìn)行包裝,以便找到對應(yīng)的方法實(shí)現(xiàn)。它的數(shù)據(jù)結(jié)構(gòu)是:
typedef struct objc_selector *SEL;
我們可以看出它是個(gè)映射到方法的 C 字符串,你可以通過 Objc 編譯器器命令@selector()
或者 Runtime 系統(tǒng)的 sel_registerName
函數(shù)來獲取一個(gè) SEL
類型的方法選擇器。
注意:
不同類中相同名字的方法所對應(yīng)的 selector 是相同的,由于變量的類型不同,所以不會導(dǎo)致它們調(diào)用方法實(shí)現(xiàn)混亂。
id
id 是一個(gè)參數(shù)類型,它是指向某個(gè)類的實(shí)例的指針。定義如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
以上定義,看到 objc_object
結(jié)構(gòu)體包含一個(gè) isa 指針,根據(jù) isa 指針就可以找到對象所屬的類。
注意:
isa 指針在代碼運(yùn)行時(shí)并不總指向?qū)嵗龑ο笏鶎俚念愋停圆荒芤揽克鼇泶_定類型,要想確定類型還是需要用對象的-class
方法。
PS:KVO 的實(shí)現(xiàn)機(jī)理就是將被觀察對象的 isa 指針指向一個(gè)中間類而不是真實(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
可以看到,一個(gè)運(yùn)行時(shí)類中關(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;
}
由此可見,我們可以動(dòng)態(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
則是存儲了單個(gè)成員變量的信息;同理,objc_method_list
結(jié)構(gòu)體存儲著方法數(shù)組的列表,而單個(gè)方法的信息則由 objc_method
結(jié)構(gòu)體存儲。
值得注意的時(shí),objc_class
中也有一個(gè) isa 指針,這說明 Objc 類本身也是一個(gè)對象。為了處理類和對象的關(guān)系,Runtime 庫創(chuàng)建了一種叫做 Meta Class(元類) 的東西,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數(shù)據(jù)。
我們所熟悉的類方法,就源自于 Meta Class。我們可以理解為類方法就是類對象的實(shí)例方法。每個(gè)類僅有一個(gè)類對象,而每個(gè)類對象僅有一個(gè)與之相關(guān)的元類。
當(dāng)你發(fā)出一個(gè)類似 [NSObject alloc](類方法)
的消息時(shí),實(shí)際上,這個(gè)消息被發(fā)送給了一個(gè)類對象(Class Object),這個(gè)類對象必須是一個(gè)元類的實(shí)例,而這個(gè)元類同時(shí)也是一個(gè)根元類(Root Meta Class)的實(shí)例。所有元類的 isa 指針最終都指向根元類。
所以當(dāng) [NSObject alloc]
這條消息發(fā)送給類對象的時(shí)候,運(yùn)行時(shí)代碼 objc_msgSend()
會去它元類中查找能夠響應(yīng)消息的方法實(shí)現(xiàn),如果找到了,就會對這個(gè)類對象執(zhí)行方法調(diào)用。
上圖實(shí)現(xiàn)是 super_class
指針,虛線時(shí) isa
指針。而根元類的父類是 NSObject
,isa
指向了自己。而 NSObject
沒有父類。
最后 objc_class
中還有一個(gè) objc_cache
,緩存,它的作用很重要,后面會提到。
Method
Method 代表類中某個(gè)方法的類型
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
是個(gè) char 指針,存儲方法的參數(shù)類型和返回值類型 -
method_imp
指向了方法的實(shí)現(xiàn),本質(zhì)是一個(gè)函數(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, ...);
它就是一個(gè)函數(shù)指針,這是由編譯器生成的。當(dāng)你發(fā)起一個(gè) ObjC 消息之后,最終它會執(zhí)行的那段代碼,就是由這個(gè)函數(shù)指針指定的。而 IMP
這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)。
如果得到了執(zhí)行某個(gè)實(shí)例某個(gè)方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面 Cache
中會提到。
你會發(fā)現(xiàn) IMP
指向的方法與 objc_msgSend
函數(shù)類型相同,參數(shù)都包含 id
和 SEL
類型。每個(gè)方法名都對應(yīng)一個(gè) SEL
類型的方法選擇器,而每個(gè)實(shí)例對象中的 SEL
對應(yīng)的方法實(shí)現(xiàn)肯定是唯一的,通過一組 id
和 SEL
參數(shù)就能確定唯一的方法實(shí)現(xiàn)地址。
而一個(gè)確定的方法也只有唯一的一組 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í)例對象接收到一個(gè)消息時(shí),它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應(yīng)的方法,因?yàn)槊看味家檎倚侍土耍莾?yōu)先在 Cache 中查找。
Runtime 系統(tǒng)會把被調(diào)用的方法存到 Cache 中,如果一個(gè)方法被調(diào)用,那么它有可能今后還會被調(diào)用,下次查找的時(shí)候就會效率更高。就像計(jì)算機(jī)組成原理中 CPU 繞過主存先訪問 Cache 一樣。
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個(gè)更常用
可以通過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)
注意:
返回的是屬性列表,列表中每個(gè)元素都是一個(gè)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
以上是一個(gè) Person 類,有3個(gè)屬性。讓我們用上述方法獲取類的運(yùn)行時(shí)屬性。
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í)才會與方法實(shí)現(xiàn)進(jìn)行綁定。
這里要清楚一點(diǎn),objc_msgSend
方法看清來好像返回了數(shù)據(jù),其實(shí)objc_msgSend
從不返回?cái)?shù)據(jù),而是你的方法在運(yùn)行時(shí)實(shí)現(xiàn)被調(diào)用后才會返回?cái)?shù)據(jù)。下面詳細(xì)敘述消息發(fā)送的步驟(如下圖):
- 首先檢測這個(gè)
selector
是不是要忽略。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù)。 - 檢測這個(gè)
selector
的 target 是不是nil
,Objc 允許我們對一個(gè) nil 對象執(zhí)行任何方法不會 Crash,因?yàn)檫\(yùn)行時(shí)會被忽略掉。 - 如果上面兩步都通過了,那么就開始查找這個(gè)類的實(shí)現(xiàn)
IMP
,先從 cache 里查找,如果找到了就運(yùn)行對應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼。 - 如果 cache 找不到就找類的方法列表中是否有對應(yīng)的方法。
- 如果類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類為止。
- 如果還找不到,就要開始進(jìn)入動(dòng)態(tài)方法解析了,后面會提到。
在消息的傳遞中,編譯器會根據(jù)情況在 objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, objc_msgSendSuper_stret
這四個(gè)方法中選擇一個(gè)調(diào)用。如果消息是傳遞給父類,那么會調(diào)用名字帶有 Super 的函數(shù),如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時(shí),會調(diào)用名字帶有 stret 的函數(shù)。
方法中的隱藏參數(shù)
疑問:
我們經(jīng)常用到關(guān)鍵字self
,但是self
是如何獲取當(dāng)前方法的對象呢?
其實(shí),這也是 Runtime 系統(tǒng)的作用,self
實(shí)在方法運(yùn)行時(shí)被動(dòng)態(tài)傳入的。
當(dāng) objc_msgSend
找到方法對應(yīng)實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn),并將消息中所有參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí),它還將傳遞兩個(gè)隱藏參數(shù):
- 接受消息的對象(
self
所指向的內(nèi)容,當(dāng)前方法的對象指針) - 方法選擇器(
_cmd
指向的內(nèi)容,當(dāng)前方法的 SEL 指針)
因?yàn)樵谠创a方法的定義中,我們并沒有發(fā)現(xiàn)這兩個(gè)參數(shù)的聲明。它們時(shí)在代碼被編譯時(shí)被插入方法實(shí)現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?/p>
這兩個(gè)參數(shù)中, self
更實(shí)用。它是在方法實(shí)現(xiàn)中訪問消息接收者對象的實(shí)例變量的途徑。
這時(shí)我們可能會想到另一個(gè)關(guān)鍵字 super
,實(shí)際上 super
關(guān)鍵字接收到消息時(shí),編譯器會創(chuàng)建一個(gè) objc_super
結(jié)構(gòu)體:
struct objc_super { id receiver; Class class; };
這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類。 receiver
仍然是 self
本身,當(dāng)我們想通過 [super class]
獲取父類時(shí),編譯器其實(shí)是將指向 self
的 id
指針和 class
的 SEL 傳遞給了 objc_msgSendSuper
函數(shù)。只有在 NSObject
類中才能找到 class
方法,然后 class
方法底層被轉(zhuǎn)換為 object_getClass()
, 接著底層編譯器將代碼轉(zhuǎn)換為 objc_msgSend(objc_super->receiver, @selector(class))
,傳入的第一個(gè)參數(shù)是指向 self
的 id
指針,與調(diào)用 [self class]
相同,所以我們得到的永遠(yuǎn)都是 self
的類型。因此你會發(fā)現(xiàn):
// 這句話并不能獲取父類的類型,只能獲取當(dāng)前類的類型名
NSLog(@"%@", NSStringFromClass([super class]));
獲取方法地址
NSObject
類中有一個(gè)實(shí)例方法:methodForSelector
,你可以用它來獲取某個(gè)方法選擇器對應(yīng)的 IMP
,舉個(gè)例子:
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í),兩個(gè)隱藏參數(shù)也必須明確給出,上面的例子調(diào)用了1000次函數(shù),你也可以嘗試給 target
發(fā)送1000次 setFilled:
消息會花多久。
雖然可以更高效的調(diào)用方法,但是這種做法很少用,除非時(shí)需要持續(xù)大量重復(fù)調(diào)用某個(gè)方法的情況,才會選擇使用以免消息發(fā)送泛濫。
注意:
methodForSelector:
方法是由 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性
動(dòng)態(tài)方法解析
你可以動(dòng)態(tài)提供一個(gè)方法實(shí)現(xiàn)。如果我們使用關(guān)鍵字 @dynamic
在類的實(shí)現(xiàn)文件中修飾一個(gè)屬性,表明我們會為這個(gè)屬性動(dòng)態(tài)提供存取方法,編譯器不會再默認(rèn)為我們生成這個(gè)屬性的 setter 和 getter 方法了,需要我們自己提供。
@dynamic propertyName;
這時(shí),我們可以通過分別重載 resolveInstanceMethod:
和 resolveClassMethod:
方法添加實(shí)例方法實(shí)現(xiàn)和類方法實(shí)現(xiàn)。
當(dāng) Runtime 系統(tǒng)在 Cache 和類的方法列表(包括父類)中找不到要執(zhí)行的方法時(shí),Runtime 會調(diào)用 resolveInstanceMethod:
或 resolveClassMethod:
來給我們一次動(dòng)態(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ù),這個(gè)符號表示的含義見:Type Encoding
注意:
動(dòng)態(tài)方法解析會在消息轉(zhuǎn)發(fā)機(jī)制侵入前執(zhí)行,動(dòng)態(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ì)入消息轉(zhuǎn)發(fā)機(jī)制(forwardInvocation:
),否則將向返回的對象重新發(fā)送消息。
轉(zhuǎn)發(fā)
當(dāng)動(dòng)態(tài)方法解析不做處理返回 NO
時(shí),則會觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制。這時(shí) forwardInvocation:
方法會被執(zhí)行,我們可以重寫這個(gè)方法來自定義我們的轉(zhuǎn)發(fā)邏輯:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
唯一參數(shù)是個(gè) NSInvocation
類型的對象,該對象封裝了原始的消息和消息的參數(shù)。我們可以實(shí)現(xiàn) forwardInvocation:
方法來對不能處理的消息做一些處理。也可以將消息轉(zhuǎn)發(fā)給其他對象處理,而不拋出錯(cuò)誤。
注意:參數(shù)
anInvocation 是從哪來的?
在forwardInvocation:
消息發(fā)送前,Runtime 系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector:
消息,并取到返回的方法簽名用于生成 NSInvocation 對象。所以重寫forwardInvocation:
的同時(shí)也要重寫methodSignatureForSelector:
方法,否則會拋異常。
當(dāng)一個(gè)對象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法相應(yīng)某消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過 forwardInvocation:
消息通知該對象。每個(gè)對象都繼承了 forwardInvocation:
方法。但是, NSObject
中的方法實(shí)現(xiàn)只是簡單的調(diào)用了 doesNotRecognizeSelector:
。通過實(shí)現(xiàn)自己的 forwardInvocation:
方法,我們可以將消息轉(zhuǎn)發(fā)給其他對象。
forwardInvocation:
方法就是一個(gè)不能識別消息的分發(fā)中心,將這些不能識別的消息轉(zhuǎn)發(fā)給不同的接收對象,或者轉(zhuǎn)發(fā)給同一個(gè)對象,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息,因此沒有響應(yīng)也不會報(bào)錯(cuò)。這一切都取決于方法的具體實(shí)現(xiàn)。
注意:
forwardInvocation:
方法只有在消息接收對象中無法正常響應(yīng)消息時(shí)才會被調(diào)用。所以,如果我們向往一個(gè)對象將一個(gè)消息轉(zhuǎn)發(fā)給其他對象時(shí),要確保這個(gè)對象不能有該消息的所對應(yīng)的方法。否則,forwardInvocation:
將不可能被調(diào)用。
轉(zhuǎn)發(fā)和多繼承
轉(zhuǎn)發(fā)和繼承相似,可用于為 Objc 編程添加一些多繼承的效果。就像下圖那樣,一個(gè)對象把消息轉(zhuǎn)發(fā)出去,就好像它把另一個(gè)對象中的方法接過來或者“繼承”過來一樣。
這使得在不同繼承體系分支下的兩個(gè)類可以實(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)致單個(gè)類變得臃腫復(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)錯(cuò),因?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:
同樣也要加入到這一行列中。
如果一個(gè)對象想要轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息,它得給出一個(gè)方法標(biāo)簽來返回準(zhǔn)確的方法描述 methodSignatureForSelector:
,這個(gè)方法會最終響應(yīng)被轉(zhuǎn)發(fā)的消息。從而生成一個(gè)確定的 NSInvocation
對象描述消息和消息參數(shù)。這個(gè)方法最終響應(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)一個(gè)類被編譯時(shí),實(shí)例變量的內(nèi)存布局就形成了,它表明訪問類的實(shí)例變量的位置。實(shí)例變量一次根據(jù)自己所占空間而產(chǎn)生位移:
上圖左是 NSObject
類的實(shí)例變量布局。右邊是我們寫的類的布局。這樣子有一個(gè)很大的缺陷,就是缺乏拓展性。哪天蘋果更新了 NSObject
類的話,就會出現(xiàn)問題:
我們自定義的類的區(qū)域和父類的區(qū)域重疊了。只有蘋果將父類改為以前的布局才能拯救我們,但這樣導(dǎo)致它們不能再拓展它們的框架了,因?yàn)槌蓡T變量布局被固定住了。在脆弱的實(shí)例變量(Fragile ivar)環(huán)境下,需要我們重新編譯繼承自 Apple 的類來恢復(fù)兼容。如果是健壯的實(shí)例變量的話,如下圖:
[圖片上傳失敗...(image-8cf21-1510822320510)]
在健壯的實(shí)例變量下,編譯器生成的實(shí)例變量布局跟以前一樣,但是當(dāng) Runtime 系統(tǒng)檢測到與父類有部分重疊時(shí)它會調(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)帶來的便利。
雖然平時(shí)我們很少會考慮一句簡單的調(diào)用方法,發(fā)送消息底層所做的復(fù)雜的操作,但深入理解 Runtime 系統(tǒng)的細(xì)節(jié)使得我們可以利用消息機(jī)制寫出功能更強(qiáng)大的代碼。