一、Runtime簡介
Runtime簡稱運(yùn)行時。OC就是運(yùn)行時機(jī)制,也就是在運(yùn)行時候的一些機(jī)制,其中最主要的是消息機(jī)制。
- 對于C語言,函數(shù)的調(diào)用在編譯的時候會決定調(diào)用哪個函數(shù)。
- 對于OC的函數(shù),屬于
動態(tài)調(diào)用過程
,在編譯的時候并不能決定真正調(diào)用哪個函數(shù),只有在真正運(yùn)行的時候才會根據(jù)函數(shù)的名稱找到對應(yīng)的函數(shù)來調(diào)用。 - 事實(shí)證明:
- 在編譯階段,OC可以調(diào)用任何函數(shù),即使這個函數(shù)并未實(shí)現(xiàn),只要聲明過就不會報錯。
- 在編譯階段,C語言調(diào)用未實(shí)現(xiàn)的函數(shù)就會報錯。
- 如果向某個對象傳遞消息,在底層,所有的方法都是普通的C語言函數(shù),然而對象收到消息之后,究竟該調(diào)用哪個方法則完全取決于運(yùn)行期決定,甚至可能在運(yùn)行期改變,這些特性使得Objective-C變成一門真正的動態(tài)語言。
- 在Runtime中,對象可以用C語言中的結(jié)構(gòu)體表示,而方法可以用C函數(shù)來實(shí)現(xiàn),另外再加上了一些額外的特性。這些結(jié)構(gòu)體和函數(shù)被Runtime函數(shù)封裝后,讓OC的面向?qū)ο缶幊套優(yōu)榭赡堋?/li>
二、Objective-C中的數(shù)據(jù)結(jié)構(gòu)
描述Objective-C對象所有的數(shù)據(jù)結(jié)構(gòu)定義都在Runtime的頭文件里,下面我們逐一分析。
1.id
運(yùn)行期系統(tǒng)如何知道某個對象的類型呢?對象類型并不是在編譯期就知道了,而是要在運(yùn)行期查找。Objective-C有個特殊的類型id,它可以表示Objective-C的任意對象類型,id類型定義在Runtime的頭文件中:
struct objc_object {
Class isa;
} *id;
由此可見,每個對象結(jié)構(gòu)體的首個成員是Class類的變量。該變量定義了對象所屬的類,通常稱為isa指針。
objc_object
objc_object是表示一個類的實(shí)例的結(jié)構(gòu)體
它的定義如下(objc/objc.h):
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
可以看到,這個結(jié)構(gòu)體只有一個字體,即指向其類的isa指針。這樣,當(dāng)我們向一個Objective-C對象發(fā)送消息時,運(yùn)行時庫會根據(jù)實(shí)例對象的isa指針找到這個實(shí)例對象所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應(yīng)的selector指向的方法,找到后即運(yùn)行這個方法。
2.Class
Class對象也定義在Runtime的頭文件中,查看objc/runtime.h中的objc_class結(jié)構(gòu)體:
Objective-C中,類是由Class類型來表示的,它實(shí)際上是一個指
向objc_class結(jié)構(gòu)體的指針。
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認(rèn)為0
long info OBJC2_UNAVAILABLE; // 類信息,供運(yùn)行期使用的一些位標(biāo)識
long instance_size OBJC2_UNAVAILABLE; // 該類的實(shí)例變量大小
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; // 協(xié)議鏈表
#endif
}
下面說下Class的結(jié)構(gòu)體中的幾個主要變量:
- 1.isa:
結(jié)構(gòu)體的首個變量也是isa指針,這說明Class本身也是Objective-C中的對象。isa指針非常重要, 對象需要通過isa指針找到它的類, 類需要通過isa找到它的元類. 這在調(diào)用實(shí)例方法和類方法的時候起到重要的作用. - 2.super_class:
結(jié)構(gòu)體里還有個變量是super_class,它定義了本類的超類。類對象所屬類型(isa指針?biāo)赶虻念愋停┦橇硗庖粋€類,叫做“元類”。 - 3.ivars:
成員變量列表,類的成員變量都在ivars里面。 - 4.methodLists:
方法列表,類的實(shí)例方法都在methodLists里,類方法在元類的methodLists里面。methodLists是一個指針的指針,通過修改該指針指向指針的值,就可以動態(tài)的為某一個類添加成員方法。這也就是Category實(shí)現(xiàn)的原理,同時也說明了Category只可以為對象添加成員方法,不能添加成員變量。 - 5.cache:
方法緩存列表,objc_msgSend(下文詳解)每調(diào)用一次方法后,就會把該方法緩存到cache列表中,下次調(diào)用的時候,會優(yōu)先從cache列表中尋找,如果cache沒有,才從methodLists中查找方法。提高效率。
元類(Meta Class)
meta-class是一個類對象的類。
在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發(fā)送消息(即調(diào)用類方法)。既然是對象,那么它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那么,這個isa指針指向什么呢?
為了調(diào)用類方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結(jié)構(gòu)體。這就引出了meta-class的概念,meta-class中存儲著一個類的所有類方法。
所以,調(diào)用類方法的這個類對象的isa指針指向的就是meta-class
當(dāng)我們向一個對象發(fā)送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發(fā)送消息時,會在這個類的meta-class的方法列表中查找。
再深入一下,meta-class也是一個類,也可以向它發(fā)送一個消息,那么它的isa又是指向什么呢?為了不讓這種結(jié)構(gòu)無限延伸下去,Objective-C的設(shè)計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。
即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。
通過上面的描述,再加上對objc_class結(jié)構(gòu)體中super_class指針的分析,我們就可以描繪出類及相應(yīng)meta-class類的一個繼承體系了,如下代碼
看圖說話:
上圖中:superclass指針代表繼承關(guān)系,isa指針代表實(shí)例所屬的類。
類也是一個對象,它是另外一個類的實(shí)例,這個就是“元類”,元類里面保存了類方法的列表,類里面保存了實(shí)例方法的列表。實(shí)例對象的isa指向類,類對象的isa指向元類,元類對象的isa指針指向一個“根元類”(root metaclass)。所有子類的元類都繼承父類的元類,換而言之,類對象和元類對象有著同樣的繼承關(guān)系。
1.Class是一個指向objc_class結(jié)構(gòu)體的指針,而id是一個指向objc_object結(jié)構(gòu)體的指針,其中的isa是一個指向objc_class結(jié)構(gòu)體的指針。其中的id就是我們所說的對象,Class就是我們所說的類。
2.isa指針不總是指向?qū)嵗龑ο笏鶎俚念悾荒芤揽克鼇泶_定類型,而是應(yīng)該用isKindOfClass:方法來確定實(shí)例對象的類。因?yàn)镵VO的實(shí)現(xiàn)機(jī)制就是將被觀察對象的isa指針指向一個中間類而不是真實(shí)的類。
Category
Category是表示一個指向分類的結(jié)構(gòu)體的指針,其定義如下:
typedef struct objc_category *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; // 分類所實(shí)現(xiàn)的協(xié)議列表
}
這個結(jié)構(gòu)體主要包含了分類定義的實(shí)例方法與類方法,其中instance_methods列表是objc_class中方法列表的一個子集,而class_methods列表是元類方法列表的一個子集。
可發(fā)現(xiàn),類別中沒有ivar成員變量指針,也就意味著:類別中不能夠添加實(shí)例變量和屬性
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表
3.SEL
//// http://www.lxweimin.com/p/3e050ec3b759
SEL是選擇子的類型,選擇子指的就是方法的名字。在Runtime的頭文件中的定義如下:
typedef struct objc_selector *SEL;
它就是個映射到方法的C字符串,SEL類型代表著方法的簽名,在類對象的方法列表中存儲著該簽名與方法代碼的對應(yīng)關(guān)系,每個方法都有一個與之對應(yīng)的SEL類型的對象,根據(jù)一個SEL對象就可以找到方法的地址,進(jìn)而調(diào)用方法。
////http://www.lxweimin.com/p/adf0d566c887
SEL又叫選擇器,是表示一個方法的selector的指針,其定義如下:
方法的selector用于表示運(yùn)行時方法的名字。Objective-C在編譯時,會依據(jù)每一個方法的名字、參數(shù)序列,生成一個唯一的整型標(biāo)識(Int類型的地址),這個標(biāo)識就是SEL。
兩個類之間,只要方法名相同,那么方法的SEL就是一樣的,每一個方法都對應(yīng)著一個SEL。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數(shù)類型不同也不行
如在某一個類中定義以下兩個方法: 錯誤
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
當(dāng)然,不同的類可以擁有相同的selector,這個沒有問題。不同類的實(shí)例對象執(zhí)行相同的selector時,會在各自的方法列表中去根據(jù)selector去尋找自己對應(yīng)的IMP。
工程中的所有的SEL組成一個Set集合,如果我們想到這個方法集合中查找某個方法時,只需要去找到這個方法對應(yīng)的SEL就行了,SEL實(shí)際上就是根據(jù)方法名hash化了的一個字符串,而對于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比!
本質(zhì)上,SEL只是一個指向方法的指針(準(zhǔn)確的說,只是一個根據(jù)方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。
@selector()就是取類方法的編號
通過下面三種方法可以獲取SEL:
a、sel_registerName函數(shù)
b、Objective-C編譯器提供的@selector()
c、NSSelectorFromString()方法
4.Method
Method代表類中的某個方法的類型,在Runtime的頭文件中的定義如下:
typedef struct objc_method *Method;
objc_method的結(jié)構(gòu)體定義如下:
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法實(shí)現(xiàn)
}
- 1.method_name:方法名。
- 2.method_types:方法類型,主要存儲著方法的參數(shù)類型和返回值類型。
- 3.IMP:方法的實(shí)現(xiàn),函數(shù)指針。(下文詳解)
class_copyMethodList(Class cls, unsigned int *outCount)
可以使用這個方法獲取某個類的成員方法列表。
////
Method用于表示類定義中的方法
我們可以看到該結(jié)構(gòu)體中包含一個SEL和IMP,實(shí)際上相當(dāng)于在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應(yīng)的IMP,從而調(diào)用方法的實(shí)現(xiàn)代碼。
5.Ivar
Ivar代表類中實(shí)例變量的類型,在Runtime的頭文件中的定義如下:
typedef struct objc_ivar *Ivar;
objc_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
}
class_copyIvarList(Class cls, unsigned int *outCount)
可以使用這個方法獲取某個類的成員變量列表。
6.objc_property_t
objc_property_t是屬性,在Runtime的頭文件中的的定義如下:
typedef struct objc_property *objc_property_t;
class_copyPropertyList(Class cls, unsigned int *outCount)
可以使用這個方法獲取某個類的屬性列表。
7.IMP
IMP在Runtime的頭文件中的的定義如下:
typedef id (*IMP)(id, SEL, ...);
IMP是一個函數(shù)指針,它是由編譯器生成的。當(dāng)你發(fā)起一個消息后,這個函數(shù)指針決定了最終執(zhí)行哪段代碼。
////
IMP實(shí)際上是一個函數(shù)指針,指向方法實(shí)現(xiàn)的地址。
其定義如下:
id (*IMP)(id, SEL,...)
第一個參數(shù):是指向self的指針(如果是實(shí)例方法,則是類實(shí)例的內(nèi)存地址;如果是類方法,則是指向元類的指針)
第二個參數(shù):是方法選擇器(selector)
接下來的參數(shù):方法的參數(shù)列表。
前面介紹過的SEL就是為了查找方法的最終實(shí)現(xiàn)IMP的。由于每個方法對應(yīng)唯一的SEL,因此我們可以通過SEL方便快速準(zhǔn)確地獲得它所對應(yīng)的IMP,查找過程將在下面討論。取得IMP后,我們就獲得了執(zhí)行這個方法代碼的入口點(diǎn),此時,我們就可以像調(diào)用普通的C語言函數(shù)一樣來使用這個函數(shù)指針了。
8.Cache
Cache在Runtime的頭文件中的的定義如下:
typedef struct objc_cache *Cache
objc_cache的定義如下:
struct objc_cache {
unsigned int mask OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
每調(diào)用一次方法后,不會直接在isa指向的類的方法列表(methodLists)中遍歷查找能夠響應(yīng)消息的方法,因?yàn)檫@樣效率太低。它會把該方法緩存到cache列表中,下次的時候,就直接優(yōu)先從cache列表中尋找,如果cache沒有,才從isa指向的類的方法列表(methodLists)中查找方法。提高效率。
三.發(fā)送消息(objc_msgSend)
在Objective-C中,調(diào)用方法是經(jīng)常使用的。用Objective-C的術(shù)語來說,這叫做“傳遞消息”(pass a message)。消息有“名稱”(name)或者“選擇子”(selector),也可以接受參數(shù),而且可能還有返回值。
如果向某個對象傳遞消息,在底層,所有的方法都是普通的C語言函數(shù),然而對象收到消息之后,究竟該調(diào)用哪個方法則完全取決于運(yùn)行期決定,甚至可能在運(yùn)行期改變,這些特性使得Objective-C變成一門真正的動態(tài)語言。
給對象發(fā)送消息可以這樣來寫:
id returnValue = [someObject message:parm];
someObject叫做“接收者”(receiver),message是“選擇子”(selector),選擇子和參數(shù)結(jié)合起來就叫做“消息”(message)。編譯器看到此消息后,將其轉(zhuǎn)換成C語言函數(shù)調(diào)用,所調(diào)用的函數(shù)乃是消息傳遞機(jī)制中的核心函數(shù),叫做objc_msgSend
,其原型如下:
id objc_msgSend (id self, SEL _cmd, ...);
后面的...表示這是個“參數(shù)個數(shù)可變的函數(shù)”,能接受兩個或兩個以上的參數(shù)。第一個參數(shù)是接收者(receiver),第二個參數(shù)是選擇子(selector),后續(xù)參數(shù)就是消息中傳遞的那些參數(shù)(parm),其順序不變。
編譯器會把上面的那個消息轉(zhuǎn)換成:
id returnValue objc_mgSend(someObject, @selector(message:), parm);
objc_msgSend
發(fā)送消息的原理:
- 第一步:檢測這個selector是不是要被忽略的。
- 第二步:檢測這個target對象是不是nil對象。(nil對象執(zhí)行任何一個方法都不會Crash,因?yàn)闀缓雎缘簦?/li>
- 第三步:首先會根據(jù)target(objc_object)對象的isa指針獲取它所對應(yīng)的類(objc_class)。
- 第四步:查看緩存中是否存在方法,系統(tǒng)把近期發(fā)送過的消息記錄在其中,蘋果認(rèn)為這樣可以提高效率: 優(yōu)先在類(class)的cache里面查找是否有與選擇子(selector)名稱相符的方法。
如果有,則找到objc_method中的IMP類型(函數(shù)指針)的成員method_imp去找到實(shí)現(xiàn)內(nèi)容,并執(zhí)行;
如果緩存中沒有命中,那么到該類的方法表(methodLists)查找該方法,依次從后往前查找。 - 第五步:如果沒有在類(class)找到,再到父類(super_class)查找,直至根類。
- 第六步:一旦找到與選擇子(selector)名稱相符的方法,就跳至其實(shí)現(xiàn)代碼。
- 第七步: 如果沒有找到,就會執(zhí)行消息轉(zhuǎn)發(fā)(message forwarding)的第一步動態(tài)解析。
如果是調(diào)用類方法
objc_class中的isa指向該類的元類(metaclass)
如果是調(diào)用類方法的話,那么就會利用objc_class中的成員isa找到元類(metaclass),然后尋找方法,直至根metaclass,沒有找到的話則仍然進(jìn)入動態(tài)解析。
#import <objc/message.h>
// 創(chuàng)建person對象
Person *p = [[Person alloc] init];
// 調(diào)用對象方法
[p eat];
// 本質(zhì):讓對象發(fā)送消息
objc_msgSend(p, @selector(eat));
// 調(diào)用類方法的方式:兩種
// 第一種通過類名調(diào)用
[Person eat];
// 第二種通過類對象調(diào)用
[[Person class] eat];
// 用類名調(diào)用類方法,底層會自動把類名轉(zhuǎn)換成類對象調(diào)用
// 本質(zhì):讓類對象發(fā)送消息
objc_msgSend([Person class], @selector(eat));
-
消息機(jī)制原理:對象根據(jù)方法編號SEL去映射表查找對應(yīng)的方法實(shí)現(xiàn)
image.png
四.消息轉(zhuǎn)發(fā)(message forwarding)
當(dāng)一個對象能接收一個消息時,就會走正常的方法調(diào)用流程。但如果一個對象無法接收指定消息時,又會發(fā)生什么事呢?默認(rèn)情況下,如果是以[object message]
的方式調(diào)用方法,如果object
無法響應(yīng)message
消息時,編譯器會報錯。但如果是以perform...
的形式來調(diào)用,則需要等到運(yùn)行時才能確定object
是否能接收message
消息。如果不能,則程序崩潰。
通常,當(dāng)我們不能確定一個對象是否能接收某個消息時,會先調(diào)用respondsToSelector:來判斷一下。如下代碼所示:
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
不過,我們這邊想討論下不使用respondsToSelector:
判斷的情況。這才是我們這一節(jié)的重點(diǎn)。
當(dāng)一個對象無法接收某一消息時,就會啟動所謂消息轉(zhuǎn)發(fā)(message forwarding)機(jī)制,通過這一機(jī)制,我們可以告訴對象如何處理未知的消息。默認(rèn)情況下,對象接收到未知的消息,會導(dǎo)致程序崩潰,通過控制臺,我們可以看到以下異常信息:
-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'
這段異常信息實(shí)際上是由NSObject的doesNotRecognizeSelector
方法拋出的。不過,我們可以采取一些措施,讓我們的程序執(zhí)行特定的邏輯,而避免程序的崩潰。
消息轉(zhuǎn)發(fā)機(jī)制基本上分為三個步驟:
- 1.動態(tài)方法解析
- 2.備用接收者
- 3.完整轉(zhuǎn)發(fā)
下面我們詳細(xì)討論一下這三個步驟。
動態(tài)方法解析
對象在接收到未知的消息時,首先會調(diào)用所屬類的類方法+resolveInstanceMethod:
(實(shí)例方法)或者+resolveClassMethod:
(類方法)。在這個方法中,我們有機(jī)會為該未知消息新增一個”處理方法””。不過使用該方法的前提是我們已經(jīng)實(shí)現(xiàn)了該“處理方法”,只需要在運(yùn)行時通過class_addMethod
函數(shù)動態(tài)添加到類里面就可以了。如下代碼所示:
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
void otherEat(id self, SEL cmd) {
NSLog(@"blog.yoonangel.com");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
class_addMethod(self, sel, (IMP)otherEat, "v@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
class_addMethod方法可謂是核心,那么依次來看他的參數(shù)的含義:
- first:添加到哪個類
- second:添加方法的方法編號(選擇子)
- third:添加方法的函數(shù)實(shí)現(xiàn)(IMP函數(shù)指針)
- fourth:IMP指針指向的函數(shù)返回值和參數(shù)類型
v代表無返回值void @代表id類型對象->self :代表選擇子SEL->_cmd- "v@:" v代表無返回值void,如果是i則代表int 無參數(shù)
- "i@:" 代表返回值是int類型,無參數(shù)
- "v@:i@:" 代表返回值是void類型,參數(shù)是int類型,存在一個參數(shù)(多參數(shù)依次累加)"v@:@@" 代表 兩個參數(shù)的沒有返回值。
這種方案更多的是為了實(shí)現(xiàn)@dynamic屬性。
備用接收者
如果在上一步無法處理消息,則Runtime會繼續(xù)調(diào)以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一個對象實(shí)現(xiàn)了這個方法,并返回一個非nil的結(jié)果,則這個對象會作為消息的新接收者,且消息會被分發(fā)到這個對象。當(dāng)然這個對象不能是self
自身,否則就是出現(xiàn)無限循環(huán)。當(dāng)然,如果我們沒有指定相應(yīng)的對象來處理aSelector
,則應(yīng)該調(diào)用父類的實(shí)現(xiàn)來返回結(jié)果。
使用這個方法通常是在對象內(nèi)部,可能還有一系列其它對象能處理該消息,我們便可借這些對象來處理消息并返回,這樣在對象外部看來,還是由該對象親自處理了這一消息。如下代碼所示:
@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[SUTRuntimeMethodHelper alloc] init];
}
return self;
}
- (void)test {
[self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 將消息轉(zhuǎn)發(fā)給_helper來處理
if ([selectorString isEqualToString:@"method2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
這一步合適于我們只想將消息轉(zhuǎn)發(fā)到另一個能處理該消息的對象上。但這一步無法對消息進(jìn)行處理,如操作消息的參數(shù)和返回值。
完整消息轉(zhuǎn)發(fā)
如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。此時會調(diào)用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
運(yùn)行時系統(tǒng)會在這一步給消息接收者最后一次機(jī)會將消息轉(zhuǎn)發(fā)給其它對象。對象會創(chuàng)建一個表示消息的NSInvocation
對象,把與尚未處理的消息有關(guān)的全部細(xì)節(jié)都封裝在anInvocation
中,包括selector,目標(biāo)(target
)和參數(shù)。我們可以在forwardInvocation
方法中選擇將消息轉(zhuǎn)發(fā)給其它對象。
forwardInvocation
:方法的實(shí)現(xiàn)有兩個任務(wù):
- 1.定位可以響應(yīng)封裝在anInvocation中的消息的對象。這個對象不需要能處理所有未知消息。
- 2.使用
anInvocation
作為參數(shù),將消息發(fā)送到選中的對象。anInvocation
將會保留調(diào)用結(jié)果,運(yùn)行時系統(tǒng)會提取這一結(jié)果并將其發(fā)送到消息的原始發(fā)送者。
不過,在這個方法中我們可以實(shí)現(xiàn)一些更復(fù)雜的功能,我們可以對消息的內(nèi)容進(jìn)行修改,比如追回一個參數(shù)等,然后再去觸發(fā)消息。另外,若發(fā)現(xiàn)某個消息不應(yīng)由本類處理,則應(yīng)調(diào)用父類的同名方法,以便繼承體系中的每個類都有機(jī)會處理此調(diào)用請求。
還有一個很重要的問題,我們必須重寫以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
消息轉(zhuǎn)發(fā)機(jī)制使用從這個方法中獲取的信息來創(chuàng)建NSInvocation
對象。因此我們必須重寫這個方法,為給定的selector
提供一個合適的方法簽名。
完整的示例如下所示:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}
NSObject的forwardInvocation
:方法實(shí)現(xiàn)只是簡單調(diào)用了doesNotRecognizeSelector
:方法,它不會轉(zhuǎn)發(fā)任何消息。這樣,如果不在以上所述的三個步驟中處理未知消息,則會引發(fā)一個異常。
從某種意義上來講,forwardInvocation:
就像一個未知消息的分發(fā)中心,將這些未知的消息轉(zhuǎn)發(fā)給其它對象。或者也可以像一個運(yùn)輸站一樣將所有未知消息都發(fā)送給同一個接收對象。這取決于具體的實(shí)現(xiàn)。
消息轉(zhuǎn)發(fā)與多重繼承
回過頭來看第二和第三步,通過這兩個方法我們可以允許一個對象與其它對象建立關(guān)系,以處理某些未知消息,而表面上看仍然是該對象在處理消息。通過這種關(guān)系,我們可以模擬“多重繼承”的某些特性,讓對象可以“繼承”其它對象的特性來處理一些事情。不過,這兩者間有一個重要的區(qū)別:多重繼承將不同的功能集成到一個對象中,它會讓對象變得過大,涉及的東西過多;而消息轉(zhuǎn)發(fā)將功能分解到獨(dú)立的小的對象中,并通過某種方式將這些對象連接起來,并做相應(yīng)的消息轉(zhuǎn)發(fā)。
不過消息轉(zhuǎn)發(fā)雖然類似于繼承,但NSObject的一些方法還是能區(qū)分兩者。如respondsToSelector:
和isKindOfClass:
只能用于繼承體系,而不能用于轉(zhuǎn)發(fā)鏈。便如果我們想讓這種消息轉(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;
}
當(dāng)一個對象在收到無法解讀的消息之后,它會將消息實(shí)施轉(zhuǎn)發(fā)。轉(zhuǎn)發(fā)的主要步驟如下:
消息轉(zhuǎn)發(fā)步驟:
- 第一步:對象在收到無法解讀的消息后,首先調(diào)用resolveInstanceMethod:方法決定是否動態(tài)添加方法。如果返回YES,則調(diào)用class_addMethod動態(tài)添加方法,消息得到處理,結(jié)束;如果返回NO,則進(jìn)入下一步;
- 第二步:當(dāng)前接收者還有第二次機(jī)會處理未知的選擇子,在這一步中,運(yùn)行期系統(tǒng)會問:能不能把這條消息轉(zhuǎn)給其他接收者來處理。會進(jìn)入forwardingTargetForSelector:方法,用于指定備選對象響應(yīng)這個selector,不能指定為self。如果返回某個對象則會調(diào)用對象的方法,結(jié)束。如果返回nil,則進(jìn)入下一步;
- 第三步:這步我們要通過methodSignatureForSelector:方法簽名,如果返回nil,則消息無法處理。如果返回methodSignature,則進(jìn)入下一步;
- 第四步:這步調(diào)用forwardInvocation:方法,我們可以通過anInvocation對象做很多處理,比如修改實(shí)現(xiàn)方法,修改響應(yīng)對象等,如果方法調(diào)用成功,則結(jié)束。如果失敗,則進(jìn)入doesNotRecognizeSelector方法,拋出異常,此異常表示選擇子最終未能得到處理。
/**
消息轉(zhuǎn)發(fā)第一步:對象在收到無法解讀的消息后,首先調(diào)用此方法,可用于動態(tài)添加方法,方法決定是否動態(tài)添加方法。如果返回YES,則調(diào)用class_addMethod動態(tài)添加方法,消息得到處理,結(jié)束;如果返回NO,則進(jìn)入下一步;
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
/**
當(dāng)前接收者還有第二次機(jī)會處理未知的選擇子,在這一步中,運(yùn)行期系統(tǒng)會問:能不能把這條消息轉(zhuǎn)給其他接收者來處理。會進(jìn)入此方法,用于指定備選對象響應(yīng)這個selector,不能指定為self。如果返回某個對象則會調(diào)用對象的方法,結(jié)束。如果返回nil,則進(jìn)入下一步;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
/**
這步我們要通過該方法簽名,如果返回nil,則消息無法處理。如果返回methodSignature,則進(jìn)入下一步。
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"study"])
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
/**
這步調(diào)用該方法,我們可以通過anInvocation對象做很多處理,比如修改實(shí)現(xiàn)方法,修改響應(yīng)對象等,如果方法調(diào)用成功,則結(jié)束。如果失敗,則進(jìn)入doesNotRecognizeSelector方法。
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setSelector:@selector(play)];
[anInvocation invokeWithTarget:self];
}
/**
拋出異常,此異常表示選擇子最終未能得到處理。
*/
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"無法處理消息:%@", NSStringFromSelector(aSelector));
}
接收者在每一步中均有機(jī)會處理消息,步驟越靠后,處理消息的代價越大。最好在第一步就能處理完,這樣系統(tǒng)就可以把此方法緩存起來了。
五.關(guān)聯(lián)對象 (AssociatedObject)
使用場景:
可以在類別中添加屬性
有時我們需要在對象中存放相關(guān)信息,Objective-C中有一種強(qiáng)大的特性可以解決此類問題,就是“關(guān)聯(lián)對象”。
可以給某個對象關(guān)聯(lián)許多其他對象,這些對象通過“鍵”來區(qū)分。存儲對象值時,可以指明“存儲策略”,用以維護(hù)相應(yīng)地“內(nèi)存管理語義”。存儲策略由名為“objc_AssociationPolicy” 的枚舉所定義。下表中列出了該枚舉值得取值,同時還列出了與之等下的@property屬性:假如關(guān)聯(lián)對象成為了屬性,那么他就會具備對應(yīng)的語義。
1.設(shè)置關(guān)聯(lián)值
參數(shù)說明:
object:與誰關(guān)聯(lián),通常是傳self
key:唯一鍵,在獲取值時通過該鍵獲取,通常是使用static
const void *來聲明
value:關(guān)聯(lián)所設(shè)置的值
policy:內(nèi)存管理策略,比如使用copy
// 以給定的鍵和策略為某對象設(shè)置關(guān)聯(lián)對象值。
void objc_setAssociatedObject(id object, const void *key, id value, objc _AssociationPolicy policy)
2.獲取關(guān)聯(lián)值
參數(shù)說明:
object:與誰關(guān)聯(lián),通常是傳self,在設(shè)置關(guān)聯(lián)時所指定的與哪個對象關(guān)聯(lián)的那個對象
key:唯一鍵,在設(shè)置關(guān)聯(lián)時所指定的鍵
// 根據(jù)給定的鍵從某對象中獲取對應(yīng)的對象值。
id objc_getAssociatedObject(id object, const void *key)
3.取消關(guān)聯(lián)
// 移除指定對象的全部關(guān)聯(lián)對象。
void objc_removeAssociatedObjects(id object)
關(guān)聯(lián)策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0, // 表示弱引用關(guān)聯(lián),通常是基本數(shù)據(jù)類型 @property (assign) or @ property (unsafe_unretained)
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 表示強(qiáng)引用關(guān)聯(lián)對象,是線程安全的 @property (nonatomic, strong)
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 表示關(guān)聯(lián)對象copy,是線程安全的 @property (nonatomic, copy)
OBJC_ASSOCIATION_RETAIN = 01401, // 表示強(qiáng)引用關(guān)聯(lián)對象,不是線程安全的 @property (atomic, strong)
OBJC_ASSOCIATION_COPY = 01403 // 表示關(guān)聯(lián)對象copy,不是線程安全的 @property (atomic, copy)
};
六.交換方法(method swizzing)
開發(fā)使用場景
:系統(tǒng)自帶的方法功能不夠,給系統(tǒng)自帶的方法擴(kuò)展一些功能,并且保持原有的功能。方式一
:繼承系統(tǒng)的類,重寫方法.方式二
:使用runtime,交換方法.
在Objective-C中,對象收到消息之后,究竟會調(diào)用哪種方法需要在運(yùn)行期才能解析出來。查找消息的唯一依據(jù)是選擇子(selector),選擇子(selector)與相應(yīng)的方法(IMP)對應(yīng),利用Objective-C的動態(tài)特性,可以實(shí)現(xiàn)在運(yùn)行時偷換選擇子(selector)對應(yīng)的方法實(shí)現(xiàn),這就是方法交換(method swizzling)。
每個類都有一個方法列表,存放著selector的名字和方法實(shí)現(xiàn)的映射關(guān)系。IMP有點(diǎn)類似函數(shù)指針,指向具體的Method實(shí)現(xiàn)。-
交換原理:
-
交換之前:
image.png -
交換之后:
image.png
-
我們可以新增選擇子,也可以改變某個選擇子所對應(yīng)的方法實(shí)現(xiàn),還可以交換兩個選擇子所映射到的指針。
Objective-C中提供了三種API來動態(tài)替換類方法或?qū)嵗椒ǖ膶?shí)現(xiàn):
- 1.
class_replaceMethod
替換類方法的定義。
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
- 2.
method_exchangeImplementations
交換兩個方法的實(shí)現(xiàn)。
method_exchangeImplementations(Method m1, Method m2)
- 3.
method_setImplementation
設(shè)置一個方法的實(shí)現(xiàn)
method_setImplementation(Method m, IMP imp)
先說下這三個方法的區(qū)別:
-
class_replaceMethod
:當(dāng)類中沒有想替換的原方法時,該方法調(diào)用class_addMethod
來為該類增加一個新方法,也正因如此,class_replaceMethod
在調(diào)用時需要傳入types參數(shù),而其余兩個卻不需要。 -
method_exchangeImplementations
:內(nèi)部實(shí)現(xiàn)就是調(diào)用了兩次method_setImplementation
方法。
再來看看他們的使用場景:
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(willMoveToSuperview:);
SEL swizzledSelector = @selector(myWillMoveToSuperview:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
BOOL didAddMethod = class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)myWillMoveToSuperview:(UIView *)newSuperview
{
NSLog(@"WillMoveToSuperview: %@", self);
[self myWillMoveToSuperview:newSuperview];
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 需求:給imageNamed方法提供功能,每次加載圖片就判斷下圖片是否加載成功。
// 步驟一:先搞個分類,定義一個能加載圖片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
// 步驟二:交換imageNamed和imageWithName的實(shí)現(xiàn),就能調(diào)用imageWithName,間接調(diào)用imageWithName的實(shí)現(xiàn)。
UIImage *image = [UIImage imageNamed:@"123"];
}
@end
@implementation UIImage (Image)
// 加載分類到內(nèi)存的時候調(diào)用
+ (void)load
{
// 交換方法
// 獲取imageWithName方法地址
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
// 獲取imageName方法地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
// 交換方法地址,相當(dāng)于交換實(shí)現(xiàn)方式
method_exchangeImplementations(imageWithName, imageName);
}
// 不能在分類中重寫系統(tǒng)方法imageNamed,因?yàn)闀严到y(tǒng)的功能給覆蓋掉,而且分類中不能調(diào)用super.
// 既能加載圖片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
// 這里調(diào)用imageWithName,相當(dāng)于調(diào)用imageName
UIImage *image = [self imageWithName:name];
if (image == nil) {
NSLog(@"加載空的圖片");
}
return image;
}
@end
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method objectAtIndex = class_getInstanceMethod(self, @selector(objectAtIndex:));
Method db_objectAtIndex = class_getInstanceMethod(self, @selector(db_objectAtIndex:));
method_exchangeImplementations(objectAtIndex, db_objectAtIndex);
});
}
- (id)db_objectAtIndex:(NSUInteger)inex{
NSLog(@"%s",__FUNCTION__);
id item;
if ( self.count > inex ) {
item = [self db_objectAtIndex:inex];
}
else{
item = nil;
}
return item;
}
總結(jié)
1.
class_replaceMethod
,當(dāng)需要替換的方法有可能不存在時,可以考慮使用該方法。
2.method_exchangeImplementations
,當(dāng)需要交換兩個方法的時使用。
3.method_setImplementation
是最簡單的用法,當(dāng)僅僅需要為一個方法設(shè)置其實(shí)現(xiàn)方式時實(shí)現(xiàn)。
Swizzling應(yīng)該總是在+load中執(zhí)行
在Objective-C中,運(yùn)行時會自動調(diào)用每個類的兩個方法。+load
會在類初始加載時調(diào)用,+initialize
會在第一次調(diào)用類的類方法或?qū)嵗椒ㄖ氨徽{(diào)用。這兩個方法是可選的,且只有在實(shí)現(xiàn)了它們時才會被調(diào)用。由于method swizzling
會影響到類的全局狀態(tài),因此要盡量避免在并發(fā)處理中出現(xiàn)競爭的情況。+load
能保證在類的初始化過程中被加載,并保證這種改變應(yīng)用級別的行為的一致性。相比之下,+initialize
在其執(zhí)行時不提供這種保證–事實(shí)上,如果在應(yīng)用中沒為給這個類發(fā)送消息,則它可能永遠(yuǎn)不會被調(diào)用。
Swizzling應(yīng)該總是在dispatch_once中執(zhí)行
與上面相同,因?yàn)?code>swizzling會改變?nèi)譅顟B(tài),所以我們需要在運(yùn)行時采取一些預(yù)防措施。原子性就是這樣一種措施,它確保代碼只被執(zhí)行一次,不管有多少個線程。GCD的dispatch_once
可以確保這種行為,我們應(yīng)該將其作為method swizzling
的最佳實(shí)踐。
選擇器、方法與實(shí)現(xiàn)
在Objective-C中,選擇器(selector
)、方法(method
)和實(shí)現(xiàn)(implementation
)是運(yùn)行時中一個特殊點(diǎn),雖然在一般情況下,這些術(shù)語更多的是用在消息發(fā)送的過程描述中。
以下是Objective-C Runtime Reference
中的對這幾個術(shù)語一些描述:
-
Selector(typedef struct objc_selector *SEL)
:用于在運(yùn)行時中表示一個方法的名稱。一個方法選擇器是一個C字符串,它是在Objective-C運(yùn)行時被注冊的。選擇器由編譯器生成,并且在類被加載時由運(yùn)行時自動做映射操作。 -
Method(typedef struct objc_method *Method)
:在類定義中表示方法的類型 -
Implementation(typedef id (*IMP)(id, SEL, ...))
:這是一個指針類型,指向方法實(shí)現(xiàn)函數(shù)的開始位置。這個函數(shù)使用為當(dāng)前CPU架構(gòu)實(shí)現(xiàn)的標(biāo)準(zhǔn)C調(diào)用規(guī)范。每一個參數(shù)是指向?qū)ο笞陨淼闹羔?self),第二個參數(shù)是方法選擇器。然后是方法的實(shí)際參數(shù)。
理解這幾個術(shù)語之間的關(guān)系最好的方式是:一個類維護(hù)一個運(yùn)行時可接收的消息分發(fā)表;分發(fā)表中的每個入口是一個方法(Method),其中key是一個特定名稱,即選擇器(SEL),其對應(yīng)一個實(shí)現(xiàn)(IMP),即指向底層C函數(shù)的指針。
為了swizzle一個方法,我們可以在分發(fā)表中將一個方法的現(xiàn)有的選擇器映射到不同的實(shí)現(xiàn),而將該選擇器對應(yīng)的原始實(shí)現(xiàn)關(guān)聯(lián)到一個新的選擇器中。
在 Cocoa 編程中,大部分的類都繼承于 NSObject ,有些 NSObject 提供的方法僅僅是為了查詢運(yùn)動時系統(tǒng)的相關(guān)信息,這此方法都可以反查自己。比如 -isKindOfClass:
和 -isMemberOfClass:
都是用于查詢在繼承體系中的位置。 -respondsToSelector:
指明是否接受特定的消息。 +conformsToProtocol:
指明是否要求實(shí)現(xiàn)在指定的協(xié)議中聲明的方法。 -methodForSelector:
提供方法實(shí)現(xiàn)的地址。
簡單概括下Runtime的方法列表和用法
- objc_getClass 獲取類名
- objc_msgSend 調(diào)用對象的sel
- class_getClassMethod 獲取類方法
- method_exchangeImplementations 交換兩個方法
- class_addMethod 給類添加方法
- class_copyIvarList 獲取成員變量信息
- class_copyPropertyList 獲取屬性信息
- class_copyMethodList 獲取方法信息
- class_copyProtocolList 獲取協(xié)議信息
- objc_setAssociatedObject 動態(tài)關(guān)聯(lián)set方法
- objc_getAssociatedObject 動態(tài)關(guān)聯(lián)get方法
- ivar_getName 獲取變量名
char *
類型 - ivar_getTypeEncoding 獲取到屬性變量的類型詳情類型介紹