前言
runtime其實(shí)在我們?nèi)粘i_發(fā)過程中很少使用到,尤其是像我現(xiàn)在比較初級的程序猿就更用不到了。但是去面試很多面試官都會(huì)問這個(gè)問題,這就很尷尬了,這就有一種裝逼的感覺,問這些對于現(xiàn)在的我們根本用不上的東西,要是很懂,那我們完全就是大牛了嘛。但是,話雖如此,runtime是object-c底層實(shí)現(xiàn)核心,如果我們明白了,對于以后我們解決問題,理解問題有很大的幫助,所以出于什么心態(tài)我們都應(yīng)該有必要去了解,甚至在一定時(shí)候去深入理解它(這個(gè)就是以后你欲求不滿,追求技術(shù)的最高境界時(shí)~~~)。
思考
1 runtime時(shí)候干嗎的?
2 為什么要有runtime?
開始了解runtime
簡單的理解:
眾所周知,簡單而高深的C語言是面向過程的編程語言,程序是按順序依次執(zhí)行,一直到結(jié)束。而在這就為了便于說明就不得不提c++這門語言,c++和object-c兩門語言都思想差不多,都是面向?qū)ο缶幊痰?,都是基于C語言加入面向?qū)ο蟮乃枷?,但是c++和object-c在實(shí)現(xiàn)機(jī)制上有很大的差別,C++是一門靜態(tài)語言,簡而言之就是在編譯的時(shí)候編譯器就已經(jīng)把函數(shù)的地址編碼進(jìn)可執(zhí)行文件了,這個(gè)時(shí)候函數(shù)調(diào)用就知道調(diào)用那段代碼了。而object-c和C++是相反的,object-c是一門動(dòng)態(tài)語言,編譯器無法將函數(shù)的地址編碼進(jìn)可執(zhí)行文件的,所以不僅需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來動(dòng)態(tài)得創(chuàng)建類和對象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā),而是在運(yùn)行的時(shí)候根據(jù)runtime條件判斷,再去找相應(yīng)的調(diào)用的那段代碼。
總結(jié):
1 Objective-C Runtime是一個(gè)將C語言轉(zhuǎn)化為面向?qū)ο笳Z言的擴(kuò)展。
2 Objective-C是基于C語言加入了面向?qū)ο筇匦院拖⑥D(zhuǎn)發(fā)機(jī)制的動(dòng)態(tài)語言,這意味著它不僅需要一個(gè)編譯器,還需要Runtime系統(tǒng)來動(dòng)態(tài)創(chuàng)建類和對象,進(jìn)行消息發(fā)送和轉(zhuǎn)發(fā)。
3 [receiver message]真的不是一個(gè)簡簡單單的方法調(diào)用。因?yàn)檫@只是在編譯階段確定了要向接收者發(fā)送message這條消息,而receive將要如何響應(yīng)這條消息,那就要看運(yùn)行時(shí)發(fā)生的情況來決定了。
4 因?yàn)镺bjc是一門動(dòng)態(tài)語言,所以它總是想辦法把一些決定工作從編譯連接推遲到運(yùn)行時(shí)。也就是說只有編譯器是不夠的,還需要一個(gè)運(yùn)行時(shí)系統(tǒng) (runtime system) 來執(zhí)行編譯后的代碼。
與Runtime交互
Objc 從三種不同的層級上與 Runtime 系統(tǒng)進(jìn)行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函數(shù)的直接調(diào)用。
-
Objective-C源代碼
大部分情況下你就只管寫你的Objc代碼就行,runtime 系統(tǒng)自動(dòng)在幕后辛勤勞作著。
還記得引言中舉的例子吧,消息的執(zhí)行會(huì)使用到一些編譯器為實(shí)現(xiàn)動(dòng)態(tài)語言特性而創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)和函數(shù),Objc中的類、方法和協(xié)議等在 runtime 中都由一些數(shù)據(jù)結(jié)構(gòu)來定義,這些內(nèi)容在后面會(huì)講到。(比如objc_msgSend函數(shù)及其參數(shù)列表中的id和SEL都是啥) -
NSObject的方法
Cocoa 中大多數(shù)類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個(gè)抽象超類,它實(shí)現(xiàn)了一些消息轉(zhuǎn)發(fā)有關(guān)的方法,可以通過繼承它來實(shí)現(xiàn)一個(gè)其他類的替身類或是虛擬出一個(gè)不存在的類,說白了就是領(lǐng)導(dǎo)把自己展現(xiàn)給大家風(fēng)光無限,但是把活兒都交給幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它并為你定義的類提供描述內(nèi)容。NSObject還有些方法能在運(yùn)行時(shí)獲得類的信息,并檢查一些特性,比如class返回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應(yīng)指定的消息;conformsToProtocol:檢查對象是否實(shí)現(xiàn)了指定協(xié)議類的方法;methodForSelector:則返回指定方法實(shí)現(xiàn)的地址。 -
Runtime的函數(shù)
Runtime 系統(tǒng)是一個(gè)由一系列函數(shù)和數(shù)據(jù)結(jié)構(gòu)組成,具有公共接口的動(dòng)態(tài)共享庫。頭文件存放于/usr/include/objc目錄下。許多函數(shù)允許你用純C代碼來重復(fù)實(shí)現(xiàn) Objc 中同樣的功能。雖然有一些方法構(gòu)成了NSObject類的基礎(chǔ),但是你在寫 Objc 代碼時(shí)一般不會(huì)直接用到這些函數(shù)的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函數(shù)的詳細(xì)文檔。
runtime術(shù)語了解
id objc_msgSend ( id self, SEL op, ... );
這是個(gè)最基本的用于發(fā)送消息的函數(shù)。
其實(shí)編譯器會(huì)根據(jù)情況在objc_msgSend, objc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四個(gè)方法中選擇一個(gè)來調(diào)用。如果消息是傳遞給超類,那么會(huì)調(diào)用名字帶有 Super 的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時(shí),那么會(huì)調(diào)用名字帶有stret的函數(shù)。
例子:
objc_msgSend(person,@selector(sayHello));
簡單過程如下:
第一個(gè)參數(shù)是要發(fā)送消息的實(shí)例,也就是 person 對象。 objc_msgSend 會(huì)先查詢它的 methodLists 方法列表,使用第二個(gè)參數(shù) sayHello 逐個(gè)和 person 的 methodLists 中的每一個(gè)方法信息的 SEL 進(jìn)行對比,如果找到對應(yīng)的方法,就調(diào)用它所對應(yīng)的函數(shù),也就是 IMP,然后調(diào)用這個(gè)函數(shù)。
-
id和Class
objc_msgSend
第一個(gè)參數(shù)類型為id,大家對它都不陌生,它是一個(gè)指向類實(shí)例的指針:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
Class是一個(gè)指向objc_class
結(jié)構(gòu)體的指針,而id是一個(gè)指向objc_object結(jié)構(gòu)體的指針,其中的isa是一個(gè)指向objc_class
結(jié)構(gòu)體的指針。其中的id就是我們所說的對象,Class就是我們所說的類。
PS:isa指針不總是指向?qū)嵗龑ο笏鶎俚念?,不能依靠它來確定類型,而是應(yīng)該用class方法來確定實(shí)例對象的類。因?yàn)镵VO的實(shí)現(xiàn)機(jī)理就是將被觀察對象的isa指針指向一個(gè)中間類而不是真實(shí)的類,這是一種叫做 isa-swizzling 的技術(shù).
objc_class定義如下:
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; // metaclass
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認(rèn)為0,可以通過runtime函數(shù)class_setVersion或者class_getVersion進(jìn)行修改、讀取
long info OBJC2_UNAVAILABLE; // 類信息,供運(yùn)行時(shí)期使用的一些位標(biāo)識,如CLS_CLASS (0x1L) 表示該類為普通 class,其中包含實(shí)例方法和變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
long instance_size OBJC2_UNAVAILABLE; // 該類的實(shí)例變量大?。ò◤母割惱^承下來的實(shí)例變量)
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量地址列表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法地址列表,與 info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲(chǔ)實(shí)例方法,如CLS_META (0x2L),則存儲(chǔ)類方法;
struct objc_cache *cache OBJC2_UNAVAILABLE; // 緩存最近使用的方法地址,用于提升效率;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 存儲(chǔ)該類聲明遵守的協(xié)議的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */
不知道你是否注意到了objc_class
中也有一個(gè)isa對象,這是因?yàn)橐粋€(gè) ObjC 類本身同時(shí)也是一個(gè)對象,為了處理類和對象的關(guān)系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西,類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數(shù)據(jù)。類方法就定義于此處,因?yàn)檫@些方法可以理解成類對象的實(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í)例。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應(yīng)消息的類方法。所以當(dāng) [NSObject alloc]
這條消息發(fā)給類對象的時(shí)候,objc_msgSend()
會(huì)去它的元類里面去查找能夠響應(yīng)消息的方法,如果找到了,然后對這個(gè)類對象執(zhí)行方法調(diào)用。
由以上代碼可見,類與對象的區(qū)別就是類比對象多了很多特征成員,類也可以當(dāng)做一個(gè)objc_object
來對待,也就是說類和對象都是對象,分別稱作類對象(class object)和實(shí)例對象(instance object),這樣我們就可以區(qū)別對象和類了。
isa:objc_object
(實(shí)例對象)中isa指針指向的類結(jié)構(gòu)稱為class(也就是該對象所屬的類)其中存放著普通成員變量與動(dòng)態(tài)方法(“-”開頭的方法);此處isa指針指向的類結(jié)構(gòu)稱為metaclass,其中存放著static類型的成員變量與static類型的方法(“+”開頭的方法)。
super_class: 指向該類的父類的指針,如果該類是根類(如NSObject或NSProxy),那么super_class就為nil。
類與對象的繼承層次關(guān)系如下圖:
-
SEL
objc_msgSend
函數(shù)第二個(gè)參數(shù)類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區(qū)分方法的 ID,而這個(gè) ID 的數(shù)據(jù)結(jié)構(gòu)是SEL:
typedef struct objc_selector *SEL;
objc_selector的定義如下:
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;// 名稱
char *types; OBJC2_UNAVAILABLE;// 類型
};
其實(shí)它就是個(gè)映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統(tǒng)的sel_registerName函數(shù)來獲得一個(gè)SEL類型的方法選擇器。
不同類中相同名字的方法所對應(yīng)的方法選擇器是相同的,即使方法名字相同而變量類型不同也會(huì)導(dǎo)致它們具有相同的方法選擇器,于是 Objc 中方法命名有時(shí)會(huì)帶上參數(shù)類型(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。
如果你知道selector對應(yīng)的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)方法將SEL轉(zhuǎn)化為字符串,再用NSLog打印。
-
Method
Method是一種代表類中的某個(gè)方法的類型。
typedef struct objc_method *Method;
而objc_method在上面的方法列表中提到過,它存儲(chǔ)了方法名,方法類型和方法實(shí)現(xiàn):
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
方法名類型為SEL
,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
方法類型method_types
是個(gè)char指針,其實(shí)存儲(chǔ)著方法的參數(shù)類型和返回值類型。
最后一個(gè)參數(shù)是 IMP 類型,它表示這個(gè) Selector 對應(yīng)的函數(shù)的地址。 對,是函數(shù)沒錯(cuò)。 Objective-C 中定義的所有類的方法在底層實(shí)現(xiàn)上就是一個(gè)函數(shù)。
- methodLists
struct objc_method_list **methodLists
methodLists 屬性表示當(dāng)前實(shí)例的方法列表,它是一個(gè) objc_method_list 類型的結(jié)構(gòu):
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;
}
這個(gè)結(jié)構(gòu)的定義可能大家會(huì)有些地方不太明白,比如 space 屬性是干什么的。咱們可以暫時(shí)拋開這些問題,只關(guān)心和消息分發(fā)相關(guān)的屬性 - method_count 屬性表示當(dāng)前這個(gè)實(shí)例中方法的個(gè)數(shù),method_list 結(jié)構(gòu)表示當(dāng)前實(shí)例上面所有方法的列表:
struct objc_method method_list[1]
-
Ivar
Ivar是一種代表類中實(shí)例變量的類型。
typedef struct objc_ivar *Ivar;
objc_ivar的數(shù)據(jù)結(jié)構(gòu)如下:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 變量名
char *ivar_type OBJC2_UNAVAILABLE; // 變量類型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字節(jié)
#ifdef __LP64__
int space OBJC2_UNAVAILABLE; // 占用空間
#endif
}
可以根據(jù)實(shí)例查找其在類中的名字,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
-
IMP
IMP
在objc.h
中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個(gè)函數(shù)指針,這是由編譯器生成的。當(dāng)你發(fā)起一個(gè) ObjC 消息之后,最終它會(huì)執(zhí)行的那段代碼,就是由這個(gè)函數(shù)指針指定的。而 IMP 這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)。既然得到了執(zhí)行某個(gè)實(shí)例某個(gè)方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面會(huì)提到。
你會(huì)發(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)地址;反之亦然。
-
Cache
在runtime.h中Cache的定義如下:
typedef struct objc_cache *Cache
還記得之前objc_class結(jié)構(gòu)體中有一個(gè)struct objc_cache *cache吧,它到底是緩存啥的呢,先看看objc_cache的實(shí)現(xiàn):
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í),它不會(huì)直接在isa指向的類的方法列表中遍歷查找能夠響應(yīng)消息的方法,因?yàn)檫@樣效率太低了,而是優(yōu)先在Cache中查找。Runtime 系統(tǒng)會(huì)把被調(diào)用的方法存到Cache中(理論上講一個(gè)方法如果被調(diào)用,那么它有可能今后還會(huì)被調(diào)用),下次查找的時(shí)候效率更高。這根計(jì)算機(jī)組成原理中學(xué)過的 CPU 繞過主存先訪問Cache的道理挺像,而我猜蘋果為提高Cache命中率應(yīng)該也做了努力吧。
-
Catagory
這個(gè)就是我們平時(shí)所說的類別了,很熟悉吧。它可以動(dòng)態(tài)的為已存在的類添加新的方法。
它的定義如下:
typedef struct objc_category *Category;
objc_category的定義如下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 類別名稱
char *class_name OBJC2_UNAVAILABLE; // 類名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 實(shí)例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 類方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協(xié)議列表
}
-
objc_property_t
objc_property_t是屬性,它的定義如下:
typedef struct objc_property *objc_property_t;
objc_property是內(nèi)置的類型,與之關(guān)聯(lián)的還有一個(gè)objc_property_attribute_t,它是屬性的attribute,也就是其實(shí)是對屬性的詳細(xì)描述,包括屬性名稱、屬性編碼類型、原子類型/非原子類型等。它的定義如下:
typedef struct {
const char *name; // 名稱
const char *value; // 值(通常是空的)
} objc_property_attribute_t;
消息
Objc 中發(fā)送消息是用中括號([])把接收者和消息括起來,而直到運(yùn)行時(shí)才會(huì)把消息與方法實(shí)現(xiàn)綁定。消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過 selector 快速查找 IMP 的過程,有了函數(shù)指針就可以執(zhí)行對應(yīng)的方法實(shí)現(xiàn);消息轉(zhuǎn)發(fā)(Message Forwarding)是在查找 IMP 失敗后執(zhí)行一系列轉(zhuǎn)發(fā)流程的慢速通道,如果不作轉(zhuǎn)發(fā)處理,則會(huì)打日志和拋出異常。
objc_msgSend函數(shù)
此函數(shù)是消息發(fā)送必經(jīng)之路,但只要一提 objc_msgSend
,都會(huì)說它的偽代碼如下或類似的邏輯,反正就是獲取 IMP 并調(diào)用:
id objc_msgSend(id self, SEL _cmd, ...) {
Class class = object_getClass(self);
IMP imp = class_getMethodImplementation(class, _cmd);
return imp ? imp(self, _cmd, ...) : 0;
}
-
objc_msgSend匯編語言編寫
當(dāng)需要發(fā)送消息時(shí),編譯器會(huì)生成中間代碼,根據(jù)情況分別調(diào)用objc_msgSend
,objc_msgSend_stret
,objc_msgSendSuper
, 或objc_msgSendSuper_stret
其中之一。
這也是為什么`` objc_msgSend``` 要用匯編語言而不是 OC、C 或 C++ 語言來實(shí)現(xiàn),因?yàn)閱为?dú)一個(gè)方法定義滿足不了多種類型返回值,有的方法返回 id,有的返回 int??紤]到不同類型參數(shù)返回值排列組合映射不同方法簽名(method signature)的問題,那 switch 語句得老長了。。。這些原因可以總結(jié)為 Calling Convention,也就是說函數(shù)調(diào)用者與被調(diào)用者必須約定好參數(shù)與返回值在不同架構(gòu)處理器上的存取規(guī)則,比如參數(shù)是以何種順序存儲(chǔ)在棧上,或是存儲(chǔ)在哪些寄存器上。除此之外還有其他原因,比如其可變參數(shù)用匯編處理起來最方便,因?yàn)檎业?IMP 地址后參數(shù)都在棧上。要是用 C++ 傳遞可變參數(shù)那就悲劇了,prologue 機(jī)制會(huì)弄亂地址(比如 i386 上為了存儲(chǔ) ebp 向后移位 4byte),最后還要用 epilogue 打掃戰(zhàn)場。而且匯編程序執(zhí)行效率高,在 Objective-C Runtime 中調(diào)用頻率較高的函數(shù)好多都用匯編寫的。
lookUpImpOrForward函數(shù)
使用 lookUpImpOrForward 快速查找 IMP。
其實(shí)_class_lookupMethodAndLoadCache3
函數(shù)(objc_msgSend匯編語言中查找IMP的函數(shù))其實(shí)只是簡單的調(diào)用了 lookUpImpOrForward
函數(shù):
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
注意lookUpImpOrForward
調(diào)用時(shí)使用緩存參數(shù)傳入為 NO,因?yàn)橹耙呀?jīng)嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
實(shí)現(xiàn)了一套查找 IMP 的標(biāo)準(zhǔn)路徑,也就是在消息轉(zhuǎn)發(fā)(Forward)之前的邏輯。
消息發(fā)送
1 檢測這個(gè) selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會(huì) retain, release 這些函數(shù)了。
2 檢測這個(gè) target 是不是 nil 對象。ObjC 的特性是允許對一個(gè) nil 對象執(zhí)行任何一個(gè)方法不會(huì) Crash,因?yàn)闀?huì)被忽略掉。
3 如果上面兩個(gè)都過了,那就開始查找這個(gè)類的 IMP,先從 cache 里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。
4 如果 cache 找不到就找一下方法分發(fā)表。
5 如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
6 如果還找不到就要開始進(jìn)入動(dòng)態(tài)方法解析了,后面會(huì)提到。
PS:這里說的分發(fā)表其實(shí)就是Class中的方法列表,它將方法選擇器和方法實(shí)現(xiàn)地址聯(lián)系起來。
其實(shí)編譯器會(huì)根據(jù)情況在
objc_msgSend, objc_msgSend_stret
, objc_msgSendSuper
, 或objc_msgSendSuper_stret
四個(gè)方法中選擇一個(gè)來調(diào)用。如果消息是傳遞給超類,那么會(huì)調(diào)用名字帶有”Super”的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時(shí),那么會(huì)調(diào)用名字帶有”stret”的函數(shù)。排列組合正好四個(gè)方法。
值得一提的是在 i386 平臺處理返回類型為浮點(diǎn)數(shù)的消息時(shí),需要用到objc_msgSend_fpret
函數(shù)來進(jìn)行處理,這是因?yàn)榉祷仡愋蜑楦↑c(diǎn)數(shù)的函數(shù)對應(yīng)的 ABI(Application Binary Interface) 與返回整型的函數(shù)的 ABI 不兼容。此時(shí)objc_msgSend
不再適用,于是objc_msgSend_fpret
被派上用場,它會(huì)對浮點(diǎn)數(shù)寄存器做特殊處理。不過在 PPC 或 PPC64 平臺是不需要麻煩它的。
PS:有木有發(fā)現(xiàn)這些函數(shù)的命名規(guī)律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”和“return”。
方法中的隱藏參數(shù)
我們經(jīng)常在方法中使用self關(guān)鍵字來引用實(shí)例本身,但從沒有想過為什么self就能取到調(diào)用當(dāng)前方法的對象吧。其實(shí)self的內(nèi)容是在方法運(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)容)
方法選擇器(_cmd
指向的內(nèi)容)
如:
[person sayHello];
/*
最后調(diào)用sayHello時(shí),是sayHello(person,@selector(sayHello));這樣
*/
之所以說它們是隱藏的是因?yàn)樵谠创a方法的定義中并沒有聲明這兩個(gè)參數(shù)。它們是在代碼被編譯時(shí)被插入實(shí)現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈?。在下面的例子中?code>self引用了接收者對象,而_cmd
引用了方法本身的選擇器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在這兩個(gè)參數(shù)中,self 更有用。實(shí)際上,它是在方法實(shí)現(xiàn)中訪問消息接收者對象的實(shí)例變量的途徑。
而當(dāng)方法中的super關(guān)鍵字接收到消息時(shí),編譯器會(huì)創(chuàng)建一個(gè)objc_super
結(jié)構(gòu)體:
struct objc_super { id receiver; Class class; };
這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。但receiver仍然是self本身,這點(diǎn)需要注意,因?yàn)楫?dāng)我們想通過[super class]
獲取超類時(shí),編譯器只是將指向self的id指針和class的SEL傳遞給了objc_msgSendSuper
函數(shù),因?yàn)橹挥性贜SObject類才能找到class方法,然后class方法調(diào)用object_getClass()
,接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class))
,傳入的第一個(gè)參數(shù)是指向self的id指針,與調(diào)用[self class]相同,所以我們得到的永遠(yuǎn)都是self的類型。
獲取方法地址
在IMP那節(jié)提到過可以避開消息綁定而直接獲取方法的地址并調(diào)用方法。這種做法很少用,除非是需要持續(xù)大量重復(fù)調(diào)用某方法的極端情況,避開消息發(fā)送泛濫而直接調(diào)用該方法會(huì)更高效。
NSObject類中有個(gè)methodForSelector:
實(shí)例方法,你可以用它來獲取某個(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í),上節(jié)提到的兩個(gè)隱藏參數(shù)就需要我們明確給出了。上面的例子調(diào)用了1000次函數(shù),你可以試試直接給target發(fā)送1000次setFilled:消息會(huì)花多久。
PS:methodForSelector:
方法是由 Cocoa 的 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性。
動(dòng)態(tài)方法解析
你可以動(dòng)態(tài)地提供一個(gè)方法的實(shí)現(xiàn)。例如我們可以用@dynamic
關(guān)鍵字在類的實(shí)現(xiàn)文件中修飾一個(gè)屬性:
@dynamic propertyName;
這表明我們會(huì)為這個(gè)屬性動(dòng)態(tài)提供存取方法,也就是說編譯器不會(huì)再默認(rèn)為我們生成setPropertyName:
和propertyName
方法,而需要我們動(dòng)態(tài)提供。我們可以通過分別重載resolveInstanceMethod:
和resolveClassMethod:
方法分別添加實(shí)例方法實(shí)現(xiàn)和類方法實(shí)現(xiàn)。因?yàn)楫?dāng) Runtime 系統(tǒng)在Cache和方法分發(fā)表中(包括超類)找不到要執(zhí)行的方法時(shí),Runtime會(huì)調(diào)用resolveInstanceMethod:
或resolveClassMethod:
來給程序員一次動(dòng)態(tài)添加方法實(shí)現(xiàn)的機(jī)會(huì)。我們需要用class_addMethod函數(shù)完成向特定類添加特定方法實(shí)現(xiàn)的操作:
ynamicMethodIMP(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
PS:動(dòng)態(tài)方法解析會(huì)在消息轉(zhuǎn)發(fā)機(jī)制浸入前執(zhí)行。如果 respondsToSelector:
或instancesRespondToSelector:
方法被執(zhí)行,動(dòng)態(tài)方法解析器將會(huì)被首先給予一個(gè)提供該方法選擇器對應(yīng)的IMP的機(jī)會(huì)。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機(jī)制,那么就讓resolveInstanceMethod:
返回NO。
評論區(qū)有人問如何用 resolveClassMethod:
解析類方法,我將他貼出有問題的代碼做了糾正和優(yōu)化后如下,可以順便將實(shí)例方法和類方法的動(dòng)態(tài)方法解析對比下:
頭文件:
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end
m 文件:
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}
- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end
需要深刻理解[self class]
與 object_getClass(self)
甚至 object_getClass([self class])
的關(guān)系,其實(shí)并不難,重點(diǎn)在于 self 的類型:
當(dāng) self 為實(shí)例對象時(shí),[self class]
與 object_getClass(self)
等價(jià),因?yàn)榍罢邥?huì)調(diào)用后者。object_getClass([self class])
得到元類。
當(dāng) self 為類對象時(shí),[self class]
返回值為自身,還是 self。object_getClass(self)
與 object_getClass([self class])
等價(jià)。
凡是涉及到類方法時(shí),一定要弄清楚元類、selector、IMP 等概念,這樣才能做到舉一反三,隨機(jī)應(yīng)變。
消息轉(zhuǎn)發(fā)
重定向()
在消息轉(zhuǎn)發(fā)機(jī)制執(zhí)行前,Runtime 系統(tǒng)會(huì)再給我們一次偷梁換柱的機(jī)會(huì),即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector
方法替換消息的接受者為其他對象:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
畢竟消息轉(zhuǎn)發(fā)要耗費(fèi)更多時(shí)間,抓住這次機(jī)會(huì)將消息重定向給別人是個(gè)不錯(cuò)的選擇,不過千萬別返回self,因?yàn)槟菢訒?huì)死循環(huán)。 如果此方法返回nil或self,則會(huì)進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制(forwardInvocation:
);否則將向返回的對象重新發(fā)送消息。
如果想替換類方法的接受者,需要覆寫+ (id)forwardingTargetForSelector:(SEL)aSelector
方法,并返回類對象:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
轉(zhuǎn)發(fā)
當(dāng)動(dòng)態(tài)方法解析不作處理返回NO時(shí),消息轉(zhuǎn)發(fā)機(jī)制會(huì)被觸發(fā)。在這時(shí)forwardInvocation:
方法會(huì)被執(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:
方法來對不能處理的消息做一些默認(rèn)的處理,也可以將消息轉(zhuǎn)發(fā)給其他對象來處理,而不拋出錯(cuò)誤。
這里需要注意的是參數(shù)anInvocation是從哪的來的呢?其實(shí)在forwardInvocation:
消息發(fā)送前,Runtime系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector:
消息,并取到返回的方法簽名用于生成NSInvocation對象。所以我們在重寫forwardInvocation:
的同時(shí)也要重寫methodSignatureForSelector:方法,否則會(huì)拋異常。
當(dāng)一個(gè)對象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)某消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過forwardInvocation:
消息通知該對象。每個(gè)對象都從NSObject類中繼承了forwardInvocation:
方法。然而,NSObject中的方法實(shí)現(xiàn)只是簡單地調(diào)用了doesNotRecognizeSelector:
。通過實(shí)現(xiàn)我們自己的forwardInvocation:
方法,我們可以在該方法實(shí)現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對象。
forwardInvocation:方法就像一個(gè)不能識別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對象?;蛘咚部梢韵笠粋€(gè)運(yùn)輸站將所有的消息都發(fā)送給同一個(gè)接收對象。它可以將一個(gè)消息翻譯成另外一個(gè)消息,或者簡單的”吃掉“某些消息,因此沒有響應(yīng)也沒有錯(cuò)誤。forwardInvocation:
方法也可以對不同的消息提供同樣的響應(yīng),這一切都取決于方法的具體實(shí)現(xiàn)。該方法所提供是將不同的對象鏈接到消息鏈的能力。
注意: forwardInvocation:
方法只有在消息接收對象中無法正常響應(yīng)消息時(shí)才會(huì)被調(diào)用。 所以,如果我們希望一個(gè)對象將negotiate消息轉(zhuǎn)發(fā)給其它對象,則這個(gè)對象不能有negotiate方法。否則,forwardInvocation:
將不可能會(huì)被調(diào)用。
Method Swizzling
之前所說的消息轉(zhuǎn)發(fā)雖然功能強(qiáng)大,但需要我們了解并且能更改對應(yīng)類的源代碼,因?yàn)槲覀冃枰獙?shí)現(xiàn)自己的轉(zhuǎn)發(fā)邏輯。當(dāng)我們無法觸碰到某個(gè)類的源代碼,卻想更改這個(gè)類某個(gè)方法的實(shí)現(xiàn)時(shí),該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時(shí)無法達(dá)到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應(yīng)的實(shí)現(xiàn)來達(dá)到“偷天換日”的目的。跟消息轉(zhuǎn)發(fā)相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險(xiǎn),也增大了debug的難度。
交換方法的幾種實(shí)現(xiàn)方式
- 利用 method_exchangeImplementations 交換兩個(gè)方法的實(shí)現(xiàn)
- 利用 class_replaceMethod 替換方法的實(shí)現(xiàn)
- 利用 method_setImplementation 來直接設(shè)置某個(gè)方法的IMP
這里摘抄一個(gè) NSHipster 的例子:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
上面的代碼通過添加一個(gè)Tracking
類別到UIViewController
類中,將UIViewController
類的viewWillAppear:
方法和Tracking
類別中xxx_viewWillAppear:方法的實(shí)現(xiàn)相互調(diào)換。Swizzling 應(yīng)該在+ load
方法中實(shí)現(xiàn),因?yàn)?code>+ load是在一個(gè)類最開始加載時(shí)調(diào)用。dispatch_once
是GCD中的一個(gè)方法,它保證了代碼塊只執(zhí)行一次,并讓其為一個(gè)原子操作,線程安全是很重要的。
如果類中不存在要替換的方法,那就先用class_addMethod
和class_replaceMethod
函數(shù)添加和替換兩個(gè)方法的實(shí)現(xiàn);如果類中已經(jīng)有了想要替換的方法,那么就調(diào)用method_exchangeImplementations
函數(shù)交換了兩個(gè)方法的 IMP,這是蘋果提供給我們用于實(shí)現(xiàn) Method Swizzling 的便捷方法。
可能有人注意到了這行:
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
object_getClass((id)self)
與 [self class]
返回的結(jié)果類型都是 Class,但前者為元類,后者為其本身,因?yàn)榇藭r(shí) self 為 Class 而不是實(shí)例.注意 [NSObject class]
與 [object class]
的區(qū)別:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
PS:如果類中沒有想被替換實(shí)現(xiàn)的原方法時(shí),class_replaceMethod
相當(dāng)于直接調(diào)用class_addMethod
向類中添加該方法的實(shí)現(xiàn);否則調(diào)用method_setImplementation
方法,types參數(shù)會(huì)被忽略。method_exchangeImplementations
方法做的事情與如下的原子操作等價(jià):
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
最后xxx_viewWillAppear:
方法的定義看似是遞歸調(diào)用引發(fā)死循環(huán),其實(shí)不會(huì)的。因?yàn)?code>[self xxx_viewWillAppear:animated]消息會(huì)動(dòng)態(tài)找到xxx_viewWillAppear:
方法的實(shí)現(xiàn),而它的實(shí)現(xiàn)已經(jīng)被我們與viewWillAppear:
方法實(shí)現(xiàn)進(jìn)行了互換,所以這段代碼不僅不會(huì)死循環(huán),如果你把[self xxx_viewWillAppear:animated]
換成[self viewWillAppear:animated]
反而會(huì)引發(fā)死循環(huán)。
runtime下class的各種常用操作
// 獲取方法列表
unsigned int methodListCount;
Method *methodList = class_copyMethodList([self class], &methodListCount);
for (unsigned int i = 0; i < methodListCount; i++) {
Method method = methodList[i];
NSLog(@"method----->%@", NSStringFromSelector(method_getName(method)));
}
// 獲取成員變量列表
unsigned int ivarCount;
Ivar *ivarList = class_copyIvarList([self class], &ivarCount);
for (unsigned int i = 0; i < ivarCount; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar----->%@", [NSString stringWithUTF8String:ivarName]);
}
// 獲取協(xié)議列表
unsigned int protocolListCount;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &protocolListCount);
for (unsigned int i = 0; i < protocolListCount; i++) {
Protocol *protocol = protocolList[1];
const char *protocolName = protocol_getName(protocol);
NSLog(@"protocol----->%@", [NSString stringWithUTF8String:protocolName]);
}
// 獲取類方法
Method classMethod = class_getClassMethod([self class], @selector(cancelPreviousPerformRequestsWithTarget:));
NSLog(@"class method----->%@", NSStringFromSelector(method_getName(classMethod)));
// 獲取實(shí)例方法
Method instanceMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
NSLog(@"instance method----->%@", NSStringFromSelector(method_getName(instanceMethod)));
// 替換方法的實(shí)現(xiàn)
Method newMethod = class_getInstanceMethod([self class], @selector(newReplaceMethod));
class_replaceMethod([self class], @selector(oldReplaceMethod), method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
[self oldReplaceMethod];
// 交換兩個(gè)方法
Method oldChangeMethod = class_getInstanceMethod([self class], @selector(oldexchangeMethod));
Method newChangeMethod = class_getInstanceMethod([self class], @selector(newexchangeMethod));
method_exchangeImplementations(oldChangeMethod, newChangeMethod);
[self oldexchangeMethod];
[self newexchangeMethod];
總結(jié)
我們之所以讓自己的類繼承NSObject不僅僅因?yàn)樘O果幫我們完成了復(fù)雜的內(nèi)存分配問題,更是因?yàn)檫@使得我們能夠用上 Runtime 系統(tǒng)帶來的便利??赡芪覀兤綍r(shí)寫代碼時(shí)可能很少會(huì)考慮一句簡單的[receiver message]
背后發(fā)生了什么,而只是當(dāng)做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細(xì)節(jié)更有利于我們利用消息機(jī)制寫出功能更強(qiáng)大的代碼,比如 Method Swizzling 等。