文中的實驗代碼我放在了這個項目中。
以下內(nèi)容是我通過整理[這篇博客]
(http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/)加上我自己的代碼驗證代碼地址,以及拓展的東西,內(nèi)容比較多,希望大家可以先收藏再慢慢看
本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態(tài)特性,使這門古老的語言煥發(fā)生機。主要內(nèi)容如下:
- 引言
- 簡介
- 與Runtime交互
- Runtime術語
- 消息
- 動態(tài)方法解析
- 消息轉(zhuǎn)發(fā)
- 健壯的實例變量(Non Fragile ivars)
- Objective-C Associated Objects
- Method Swizzling
- 總結(jié)
引言
曾經(jīng)覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文檔和調(diào)用。還記得初學 Objective-C 時把[receiver message]當成簡單的方法調(diào)用,而無視了“發(fā)送消息”這句話的深刻含義。其實[receiver message]會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
如果消息含有參數(shù),則為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者能夠找到對應的selector,那么就相當于直接執(zhí)行了接收者這個對象的特定方法;否則,消息要么被轉(zhuǎn)發(fā),或是臨時向接收者動態(tài)添加這個selector對應的實現(xiàn)內(nèi)容,要么就干脆玩完崩潰掉。
現(xiàn)在可以看出[receiver message]真的不是一個簡簡單單的方法調(diào)用。因為這只是在編譯階段確定了要向接收者發(fā)送message這條消息,而receive將要如何響應這條消息,那就要看運行時發(fā)生的情況來決定了。
Objective-C 的 Runtime 鑄就了它動態(tài)語言的特性,這些深層次的知識雖然平時寫代碼用的少一些,但是卻是每個 Objc 程序員需要了解的。
簡介
因為Objc是一門動態(tài)語言,所以它總是想辦法把一些決定工作從編譯連接推遲到運行時。也就是說只有編譯器是不夠的,還需要一個運行時系統(tǒng) (runtime system) 來執(zhí)行編譯后的代碼。這就是 Objective-C Runtime 系統(tǒng)存在的意義,它是整個Objc運行框架的一塊基石。
Runtime其實有兩個版本:“modern”和 “l(fā)egacy”。我們現(xiàn)在用的 Objective-C 2.0 采用的是現(xiàn)行(Modern)版的Runtime系統(tǒng),只能運行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X較老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統(tǒng)。這兩個版本最大的區(qū)別在于當你更改一個類的實例變量的布局時,在早期版本中你需要重新編譯它的子類,而現(xiàn)行版就不需要。
Runtime基本是用C和匯編寫的,可見蘋果為了動態(tài)系統(tǒng)的高效而作出的努力。你可以在這里下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。
與Runtime交互
Objc 從三種不同的層級上與 Runtime 系統(tǒng)進行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函數(shù)的直接調(diào)用。
Objective-C源代碼
大部分情況下你就只管寫你的Objc代碼就行,runtime 系統(tǒng)自動在幕后辛勤勞作著。還記得引言中舉的例子吧,消息的執(zhí)行會使用到一些編譯器為實現(xiàn)動態(tài)語言特性而創(chuàng)建的數(shù)據(jù)結(jié)構和函數(shù),Objc中的類、方法和協(xié)議等在 runtime 中都由一些數(shù)據(jù)結(jié)構來定義,這些內(nèi)容在后面會講到。(比如objc_msgSend
函數(shù)及其參數(shù)列表中的id和SEL都是啥)
NSObject的方法
Cocoa 中大多數(shù)類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現(xiàn)了一些消息轉(zhuǎn)發(fā)有關的方法,可以通過繼承它來實現(xiàn)一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現(xiàn)給大家風光無限,但是把活兒都交給幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它并為你定義的類提供描述內(nèi)容。
NSObject還有些方法能在運行時獲得類的信息,并檢查一些特性,比如class返回對象的類;
isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應指定的消息;
conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法;
methodForSelector:則返回指定方法實現(xiàn)的地址。
Runtime的函數(shù)
Runtime 系統(tǒng)是一個由一系列函數(shù)和數(shù)據(jù)結(jié)構組成,具有公共接口的動態(tài)共享庫。頭文件存放于/usr/include/objc目錄下。(NSObject類也存在此目錄下,而foundation里面存儲了NSObject的category擴展類)


許多函數(shù)允許你用純C代碼來重復實現(xiàn) Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 代碼時一般不會直接用到這些函數(shù)的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函數(shù)的詳細文檔。
Runtime術語
還記得引言中的objc_msgSend:方法吧,它的真身是這樣的:
id objc_msgSend ( id self, SEL op, ... );

下面將會逐漸展開介紹一些術語,其實它們都對應著數(shù)據(jù)結(jié)構。
SEL
objc_msgSend函數(shù)第二個參數(shù)類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區(qū)分方法的 ID,而這個 ID 的數(shù)據(jù)結(jié)構是SEL:
typedef struct objc_selector *SEL;

其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()
或者 Runtime 系統(tǒng)的sel_registerName函數(shù)來獲得一個SEL類型的方法選擇器。
不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器,于是 Objc 中方法命名有時會帶上參數(shù)類型(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。
id
objc_msgSend
第一個參數(shù)類型為id,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };

objc_object
結(jié)構體包含一個isa指針,根據(jù)isa指針就可以順藤摸瓜找到對象所屬的類。
- PS:isa指針不總是指向?qū)嵗龑ο笏鶎俚念?,不能依靠它來確定類型,而是應該用class方法來確定實例對象的類。因為KVO的實現(xiàn)機理就是將被觀察對象的isa指針指向一個中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文檔
Class
之所以說isa是指針是因為Class其實是一個指向objc_class結(jié)構體的指針:
typedef struct objc_class *Class;
而objc_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;
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;
可以看到運行時一個類還關聯(lián)了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協(xié)議。
PS:OBJC2_UNAVAILABLE之類的宏定義是蘋果在 Objc 中對系統(tǒng)運行版本進行約束的黑魔法,為的是兼容非Objective-C 2.0的遺留邏輯,但我們?nèi)阅軓闹蝎@得一些有價值的信息,有興趣的可以查看源代碼。
Objective-C 2.0 的頭文件雖然沒暴露出objc_class結(jié)構體更詳細的設計,我們依然可以從Objective-C 1.0 的定義中小窺端倪:
在objc_class結(jié)構體中:
ivars是objc_ivar_list指針;
methodLists是指向objc_method_list指針的指針。也就是說可以動態(tài)修改*methodLists的值來添加成員方法,這也是Category實現(xiàn)的原理,同樣解釋了Category不能添加屬性的原因。而最新版的 Runtime 源碼對這一塊的描述已經(jīng)有很大變化,可以參考下美團技術團隊的深入理解Objective-C:Category。
PS:任性的話可以在Category中添加@dynamic的屬性,并利用運行期動態(tài)提供存取方法或干脆動態(tài)轉(zhuǎn)發(fā);或者干脆使用關聯(lián)度對(AssociatedObject)
具體可以參考這里
其中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;
}
如果你C語言不是特別好,可以直接理解為objc_ivar_list結(jié)構體存儲著objc_ivar數(shù)組列表,而objc_ivar結(jié)構體存儲了類的單個成員變量的信息;
同理objc_method_list結(jié)構體存儲著objc_method數(shù)組列表,而objc_method結(jié)構體存儲了類的某個方法的信息。
最后要提到的還有一個objc_cache,顧名思義它是緩存,它在objc_class的作用很重要,在后面會講到。
元類和根元類
不知道你是否注意到了objc_class
中也有一個isa對象,這是因為一個 ObjC 類本身同時也是一個對象,為了處理類和對象的關系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西,類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數(shù)據(jù)。類方法就定義于此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。當你發(fā)出一個類似[NSObject alloc]的消息時,你事實上是把這個消息發(fā)給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應消息的類方法。所以當 [NSObject alloc]
這條消息發(fā)給類對象的時候,objc_msgSend()會去它的元類里面去查找能夠響應消息的方法,如果找到了,然后對這個類對象執(zhí)行方法調(diào)用。
上圖實線是 super_class 指針,虛線是isa
指針。 有趣的是根元類的超類是NSObject,而isa
指向了自己,而NSObject的超類為nil,也就是它沒有超類。
Method
Method
是一種代表類中的某個方法的類型。
typedef struct objc_method *Method;
而objc_method
在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現(xiàn):
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
方法名類型為SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
方法類型method_types是個char指針,其實存儲著方法的參數(shù)類型和返回值類型。
method_imp指向了方法的實現(xiàn),本質(zhì)上是一個函數(shù)指針,后面會詳細講到。
Ivar
Ivar
是一種代表類中實例變量的類型。
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
}
可以根據(jù)實例查找其在類中的名字,也就是“反射”:
-(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;
}
-
我的代碼試驗:兩個全局變量,并封裝與一個數(shù)組中存儲,輸出的結(jié)果正確
試驗
試驗
輸出結(jié)果
輸出結(jié)果 -
我的代碼試驗:將對象的地址獲取到,并將long型的內(nèi)存地址強轉(zhuǎn)成對象,并打印出來,可以打印出對象的名字的。
實驗輸出結(jié)果
class_copyIvarList 函數(shù)獲取的不僅有實例變量,還有屬性。但會在原本的屬性名前加上一個下劃線。
- 我的拓展:


通過ivar_getName獲取到的成員變量的名字,其中屬性值會在前面有個_,例如_floatProperty



結(jié)果中上面三個值是通過遍歷ivarlist獲取到的,其中f代表MyObject的成員變量float型的floatIvar,緊接著的兩個@“NSString”代表的是內(nèi)部的成員變量stringIvar和屬性floatProperty
property_getAttributes可以獲取到如下的參數(shù),如圖

|strong|copy|weak|assign|retain|readonly|readwrite|
|---|---|---|
|&|C|W|無|&|R|&|
第三個成員 ivar_offset 。它表示基地址偏移字節(jié)。可以參照后面的健壯的實例變量(Non Fragile ivars)這一節(jié)
IMP
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個函數(shù)指針,這是由編譯器生成的。當你發(fā)起一個 ObjC 消息之后,最終它會執(zhí)行的那段代碼,就是由這個函數(shù)指針指定的。而 IMP 這個函數(shù)指針就指向了這個方法的實現(xiàn)。既然得到了執(zhí)行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面會提到。
你會發(fā)現(xiàn)IMP指向的方法與objc_msgSend函數(shù)類型相同,參數(shù)都包含id和SEL類型。每個方法名都對應一個SEL類型的方法選擇器,而每個實例對象中的SEL對應的方法實現(xiàn)肯定是唯一的,通過一組id和SEL參數(shù)就能確定唯一的方法實現(xiàn)地址;反之亦然。
Cache
在runtime.h
中Cache的定義如下:
typedef struct objc_cache *Cache
還記得之前objc_class結(jié)構體中有一個struct objc_cache *cache
吧,它到底是緩存啥的呢,先看看objc_cache
的實現(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)用的性能進行優(yōu)化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優(yōu)先在Cache中查找。Runtime 系統(tǒng)會把被調(diào)用的方法存到Cache
中(理論上講一個方法如果被調(diào)用,那么它有可能今后還會被調(diào)用),下次查找的時候效率更高。這根計算機組成原理中學過的 CPU 繞過主存先訪問Cache的道理挺像,而我猜蘋果為提高Cache命中率應該也做了努力吧。
Property
@property
標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property結(jié)構體的指針:
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)
返回類型為指向指針的指針,哈哈,因為屬性列表是個數(shù)組,每個元素內(nèi)容都是一個objc_property_t指針,而這兩個函數(shù)返回的值是指向這個數(shù)組的指針。
舉個栗子,先聲明一個類:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以用下面的代碼獲取屬性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
- 我的實驗代碼:輸出結(jié)果是一個指向內(nèi)存地址的指針

你可以用property_getName
函數(shù)來查找屬性名稱:
const char *property_getName(objc_property_t property)
你可以用class_getProperty 和 protocol_getProperty通過給出的名稱來在類和協(xié)議中獲取屬性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes函數(shù)來發(fā)掘?qū)傩缘拿Q和@encode類型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代碼放一起,你就能從一個類中獲取它的屬性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
對比下 class_copyIvarList 函數(shù),使用 class_copyPropertyList 函數(shù)只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。
- 我的實驗代碼
unsigned int proCount;
int j;
objc_property_t *properties = class_copyPropertyList(myobjectClass, &proCount);
for (j = 0; j < proCount; j ++)
{
objc_property_t property = properties[j];
const char *proName = property_getName(property);
NSString *propertyName = [NSString stringWithCString:proName encoding:NSUTF8StringEncoding];
const char *string = property_getAttributes(property);
NSString *stringProperty = [NSString stringWithCString:string encoding:NSUTF8StringEncoding];
NSLog(@"獲取到的property名字%@,參數(shù)信息%@", propertyName,stringProperty);
}
-
打印結(jié)果如下
property
消息
前面做了這么多鋪墊,現(xiàn)在終于說到了消息了。Objc 中發(fā)送消息是用中括號([])把接收者和消息括起來,而直到運行時才會把消息與方法實現(xiàn)綁定。
有關消息發(fā)送和消息轉(zhuǎn)發(fā)機制的原理,可以查看這篇文章。
objc_msgSend函數(shù)
在引言中已經(jīng)對objc_msgSend進行了一點介紹,看起來像是objc_msgSend返回了數(shù)據(jù),其實objc_msgSend從不返回數(shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細敘述下消息發(fā)送步驟:
檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain, release 這些函數(shù)了。
檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉。
如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數(shù)去執(zhí)行。
如果 cache 找不到就找一下方法分發(fā)表。
如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
如果還找不到就要開始進入動態(tài)方法解析了,后面會提到。
PS:這里說的分發(fā)表其實就是Class
中的方法列表,它將方法選擇器和方法實現(xiàn)地址聯(lián)系起來。
其實編譯器會根據(jù)情況在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四個方法中選擇一個來調(diào)用。如果消息是傳遞給超類,那么會調(diào)用名字帶有”Super”的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構而不是簡單值時,那么會調(diào)用名字帶有”stret”的函數(shù)。排列組合正好四個方法。
值得一提的是在 i386 平臺處理返回類型為浮點數(shù)的消息時,需要用到objc_msgSend_fpret函數(shù)來進行處理,這是因為返回類型為浮點數(shù)的函數(shù)對應的 ABI(Application Binary Interface) 與返回整型的函數(shù)的 ABI 不兼容。此時objc_msgSend不再適用,于是objc_msgSend_fpret被派上用場,它會對浮點數(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關鍵字來引用實例本身,但從沒有想過為什么self就能取到調(diào)用當前方法的對象吧。其實self的內(nèi)容是在方法運行時被偷偷的動態(tài)傳入的。
當objc_msgSend找到方法對應的實現(xiàn)時,它將直接調(diào)用該方法實現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏的參數(shù):
接收消息的對象(也就是self指向的內(nèi)容)
方法選擇器(_cmd指向的內(nèi)容)
之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)。它們是在代碼被編譯時被插入實現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈?。在下面的例子中,self引用了接收者對象,而_cmd
引用了方法本身的選擇器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在這兩個參數(shù)中,self 更有用。實際上,它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑。
而當方法中的super關鍵字接收到消息時,編譯器會創(chuàng)建一個objc_super結(jié)構體:
struct objc_super { id receiver; Class class; };
這個結(jié)構體指明了消息應該被傳遞給特定超類的定義。但receiver仍然是self本身,這點需要注意,因為當我們想通過[super class]獲取超類時,編譯器只是將指向self的id指針和class的SEL傳遞給了objc_msgSendSuper函數(shù),因為只有在NSObject類才能找到class方法,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver,@selector(class)),傳入的第一個參數(shù)是指向self的id指針,與調(diào)用[self class]相同,所以我們得到的永遠都是self的類型。
- 我的試驗代碼
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"[self class] - %@", NSStringFromClass([self class]));
NSLog(@"[super class] - %@", NSStringFromClass([super class]));
}
return self;
}
+ (void)initialize
{
NSLog(@"[MyObject class] - %@", NSStringFromClass([MyObject class]));
}
- 輸出結(jié)果
2017-04-24 18:04:37.410 JHRuntime[18978:598271] [MyObject class] - MyObject
2017-04-24 18:04:37.410 JHRuntime[18978:598271] [self class] - MyObject
2017-04-24 18:04:37.410 JHRuntime[18978:598271] [super class] - MyObject
獲取方法地址
在IMP那節(jié)提到過可以避開消息綁定而直接獲取方法的地址并調(diào)用方法。這種做法很少用,除非是需要持續(xù)大量重復調(diào)用某方法的極端情況,避開消息發(fā)送泛濫而直接調(diào)用該方法會更高效。
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ù)調(diào)用時,上節(jié)提到的兩個隱藏參數(shù)就需要我們明確給出了。上面的例子調(diào)用了1000次函數(shù),你可以試試直接給target發(fā)送1000次setFilled:消息會花多久。
PS:methodForSelector:方法是由 Cocoa 的 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性。
-
methodForSelector聲明在NSObject中,是個實例方法
方法地址
動態(tài)方法解析
你可以動態(tài)地提供一個方法的實現(xiàn)。例如我們可以用@dynamic關鍵字在類的實現(xiàn)文件中修飾一個屬性:
@dynamic propertyName;
這表明我們會為這個屬性動態(tài)提供存取方法,也就是說編譯器不會再默認為我們生成setPropertyName:和propertyName方法,而需要我們動態(tài)提供。我們可以通過分別重載resolveInstanceMethod:和resolveClassMethod:方法分別添加實例方法實現(xiàn)和類方法實現(xiàn)。因為當 Runtime 系統(tǒng)在Cache和方法分發(fā)表中(包括超類)找不到要執(zhí)行的方法時,Runtime會調(diào)用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
PS:動態(tài)方法解析會在消息轉(zhuǎn)發(fā)機制浸入前執(zhí)行。如果 respondsToSelector: 或 instancesRespondToSelector:方法被執(zhí)行,動態(tài)方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機制,那么就讓resolveInstanceMethod:
返回NO
。
評論區(qū)有人問如何用 resolveClassMethod:
解析類方法,我將他貼出有問題的代碼做了糾正和優(yōu)化后如下,可以順便將實例方法和類方法的動態(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
- 注意:動態(tài)方法需要綁定動態(tài)方法的實現(xiàn),類方法需要綁定類方法的實現(xiàn)
需要深刻理解 [self class] 與 object_getClass(self) 甚至 object_getClass([self class]) 的關系,其實并不難,重點在于 self 的類型:
當 self 為實例對象時,[self class] 與 object_getClass(self) 等價,因為前者會調(diào)用后者。object_getClass([self class]) 得到元類。
當 self 為類對象時,[self class] 返回值為自身,還是 self。object_getClass(self) 與 object_getClass([self class]) 等價。
凡是涉及到類方法時,一定要弄清楚元類、selector、IMP 等概念,這樣才能做到舉一反三,隨機應變。
-
我的實驗代碼
error
1,實例方法里面的self,是對象的首地址。
2,類方法里面的self,是Class.
- 我的實驗代碼
+ (void)initialize
{
[super initialize];
Class a = self;
const char *aStr = object_getClassName(a);
NSLog(@"類方法[self class] - %s", aStr);
Class b = [MyObject class];
const char *bStr = object_getClassName(b);
NSLog(@"類方法[MyObject class] - %s", bStr);
Class c = [self class];
const char *cStr = object_getClassName(c);
NSLog(@"類方法[self class] - %s", cStr);
Class d = [super class];
const char *dStr = object_getClassName(d);
NSLog(@"類方法[super class] - %s", dStr);
Class metaClass = objc_getMetaClass("MyObject");
const char *metaStr = object_getClassName(metaClass);
NSLog(@"類方法objc_getMetaClass %p---%s", metaClass,metaStr);
//objc_getClass參數(shù)是類名的字符串,返回的就是這個類的類對象;object_getClass參數(shù)是id類型,它返回的是這個id的isa指針所指向的Class,如果傳參是Class,則返回該Class的metaClass。
Class currentClass = object_getClass(self);
const char *objectStr = object_getClassName(currentClass);
NSLog(@"類方法object_getClass %p---%s", currentClass,objectStr);
Class selfClass = object_getClass([self class]);
const char *selfClassStr = object_getClassName(selfClass);
NSLog(@"類方法bject_getClass([self class]) %p---%s", selfClass,selfClassStr);
Class curClass = objc_getClass("MyObject");
const char *objcStr = object_getClassName(curClass);
NSLog(@"類方法objc_getClass %p---%s", curClass,objcStr);
}
- (instancetype)init
{
self = [super init];
if (self) {
Class a = self;
const char *aStr = object_getClassName(a);
NSLog(@"實例方法[self class] - %s", aStr);
Class b = [MyObject class];
const char *bStr = object_getClassName(b);
NSLog(@"實例方法[MyObject class] - %s", bStr);
Class c = [self class];
const char *cStr = object_getClassName(c);
NSLog(@"實例方法[self class] - %s", cStr);
Class d = [super class];
const char *dStr = object_getClassName(d);
NSLog(@"實例方法[super class] - %s", dStr);
Class currentClass = object_getClass(self);
const char *objectStr = object_getClassName(currentClass);
NSLog(@"實例方法object_getClass %p---%s", currentClass,objectStr);
Class selfClass = object_getClass([self class]);
const char *selfClassStr = object_getClassName(selfClass);
NSLog(@"實例方法bject_getClass([self class]) %p---%s", selfClass,selfClassStr);
Class curClass = objc_getClass("MyObject");
const char *objcStr = object_getClassName(curClass);
NSLog(@"實例方法objc_getClass %p---%s", curClass,objcStr);
Class metaClass = objc_getMetaClass("MyObject");
const char *metaStr = object_getClassName(metaClass);
NSLog(@"實例方法objc_getMetaClass %p---%s", metaClass,metaStr);
}
return self;
}
- 輸出結(jié)果
2017-04-25 16:10:46.404 JHRuntime[24762:871388] 類方法[self class] - MyObject
2017-04-25 16:10:46.404 JHRuntime[24762:871388] 類方法[MyObject class] - MyObject
2017-04-25 16:10:46.404 JHRuntime[24762:871388] 類方法[self class] - MyObject
2017-04-25 16:10:46.404 JHRuntime[24762:871388] 類方法[super class] - MyObject
2017-04-25 16:10:46.404 JHRuntime[24762:871388] 類方法objc_getMetaClass 0x1043e2808---NSObject
2017-04-25 16:10:46.404 JHRuntime[24762:871388] 類方法object_getClass 0x1043e2808---NSObject
2017-04-25 16:10:46.405 JHRuntime[24762:871388] 類方法bject_getClass([self class]) 0x1043e2808---NSObject
2017-04-25 16:10:46.405 JHRuntime[24762:871388] 類方法objc_getClass 0x1043e2830---MyObject
2017-04-25 16:10:46.405 JHRuntime[24762:871388] 實例方法[self class] - MyObject
2017-04-25 16:10:46.405 JHRuntime[24762:871388] 實例方法[MyObject class] - MyObject
2017-04-25 16:10:46.405 JHRuntime[24762:871388] 實例方法[self class] - MyObject
2017-04-25 16:10:46.405 JHRuntime[24762:871388] 實例方法[super class] - MyObject
2017-04-25 16:10:46.406 JHRuntime[24762:871388] 實例方法object_getClass 0x1043e2830---MyObject
2017-04-25 16:10:46.470 JHRuntime[24762:871388] 實例方法bject_getClass([self class]) 0x1043e2808---NSObject
2017-04-25 16:10:46.471 JHRuntime[24762:871388] 實例方法objc_getClass 0x1043e2830---MyObject
2017-04-25 16:10:46.471 JHRuntime[24762:871388] 實例方法objc_getMetaClass 0x1043e2808---NSObject
2017-04-25 16:10:46.471 JHRuntime[24762:871388] 實例方法[self class] - MyObject
2017-04-25 16:10:46.471 JHRuntime[24762:871388] 實例方法[MyObject class] - MyObject
2017-04-25 16:10:46.471 JHRuntime[24762:871388] 實例方法[self class] - MyObject
2017-04-25 16:10:46.472 JHRuntime[24762:871388] 實例方法[super class] - MyObject
2017-04-25 16:10:46.472 JHRuntime[24762:871388] 實例方法object_getClass 0x1043e2830---MyObject
2017-04-25 16:10:46.472 JHRuntime[24762:871388] 實例方法bject_getClass([self class]) 0x1043e2808---NSObject
2017-04-25 16:10:46.472 JHRuntime[24762:871388] 實例方法objc_getClass 0x1043e2830---MyObject
2017-04-25 16:10:46.473 JHRuntime[24762:871388] 實例方法objc_getMetaClass 0x1043e2808---NSObject
- objc_getClass參數(shù)是類名的字符串,返回的就是這個類的類對象;object_getClass參數(shù)是id類型,它返回的是這個id的isa指針所指向的Class,如果傳參是Class,則返回該Class的metaClass
- objc_getClass方法只是單純地返回了Class,而非isa指針指向的Class。
消息轉(zhuǎn)發(fā)
重定向
在消息轉(zhuǎn)發(fā)機制執(zhí)行前,Runtime 系統(tǒng)會再給我們一次偷梁換柱的機會,即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector
方法替換消息的接受者為其他對象:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
畢竟消息轉(zhuǎn)發(fā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回self,因為那樣會死循環(huán)。 如果此方法返回nil或self,則會進入消息轉(zhuǎn)發(fā)機制(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ā)
當動態(tài)方法解析不作處理返回NO
時,消息轉(zhuǎn)發(fā)機制會被觸發(fā)。在這時forwardInvocation:方法會被執(zhí)行,我們可以重寫這個方法來定義我們的轉(zhuǎn)發(fā)邏輯:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
該消息的唯一參數(shù)是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數(shù)。我們可以實現(xiàn)forwardInvocation:方法來對不能處理的消息做一些默認的處理,也可以將消息轉(zhuǎn)發(fā)給其他對象來處理,而不拋出錯誤。
這里需要注意的是參數(shù)anInvocation是從哪的來的呢?其實在forwardInvocation:消息發(fā)送前,Runtime系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation
對象。所以我們在重寫forwardInvocation:
的同時也要重寫methodSignatureForSelector:方法,否則會拋異常。
當一個對象由于沒有相應的方法實現(xiàn)而無法響應某消息時,運行時系統(tǒng)將通過forwardInvocation:
消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現(xiàn)只是簡單地調(diào)用了doesNotRecognizeSelector:。通過實現(xiàn)我們自己的forwardInvocation:方法,我們可以在該方法實現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對象。
forwardInvocation:方法就像一個不能識別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對象?;蛘咚部梢韵笠粋€運輸站將所有的消息都發(fā)送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應,這一切都取決于方法的具體實現(xiàn)。該方法所提供是將不同的對象鏈接到消息鏈的能力。
注意: forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調(diào)用。 所以,如果我們希望一個對象將negotiate
消息轉(zhuǎn)發(fā)給其它對象,則這個對象不能有negotiate方法。否則,forwardInvocation:
將不可能會被調(diào)用。
轉(zhuǎn)發(fā)和多繼承
轉(zhuǎn)發(fā)和繼承相似,可以用于為Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉(zhuǎn)發(fā)出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。
這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中Warrior和Diplomat沒有繼承關系,但是Warrior將negotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似Diplomat是Warrior的超類一樣。
消息轉(zhuǎn)發(fā)彌補了 Objc 不支持多繼承的性質(zhì),也避免了因為多繼承導致單個類變得臃腫復雜。它將問題分解得很細,只針對想要借鑒的方法才轉(zhuǎn)發(fā),而且轉(zhuǎn)發(fā)機制是透明的。
替代者對象(Surrogate Objects)
轉(zhuǎn)發(fā)不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背后是強大的男人,畢竟女人遇到難題都把它們轉(zhuǎn)發(fā)給男人來做了。這里有一些適用案例,可以參看官方文檔。
轉(zhuǎn)發(fā)與繼承
盡管轉(zhuǎn)發(fā)很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector: 和 isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉(zhuǎn)發(fā)鏈。比如上圖中一個Warrior對象如果被問到是否能響應negotiate消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
結(jié)果是NO,盡管它能夠接受negotiate消息而不報錯,因為它靠轉(zhuǎn)發(fā)消息給Diplomat類來響應消息。
如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior繼承到了Diplomat的negotiate方法,你得重新實現(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:中也應該寫一份轉(zhuǎn)發(fā)算法。如果使用了協(xié)議,conformsToProtocol:
同樣也要加入到這一行列中。類似地,如果一個對象轉(zhuǎn)發(fā)它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉(zhuǎn)發(fā)的消息。比如一個對象能給它的替代者對象轉(zhuǎn)發(fā)消息,它需要像下面這樣實現(xiàn)methodSignatureForSelector::
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
健壯的實例變量(Non Fragile ivars)
在 Runtime 的現(xiàn)行版本中,最大的特點就是健壯的實例變量。當一個類被編譯時,實例變量的布局也就形成了,它表明訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據(jù)自己所占空間而產(chǎn)生位移:
上圖左邊是NSObject
類的實例變量布局,右邊是我們寫的類的布局,也就是在超類后面加上我們自己類的實例變量,看起來不錯。但試想如果哪天蘋果更新了NSObject
類,發(fā)布新版本的系統(tǒng)的話,那就悲劇了:

我們自定義的類被劃了兩道線,那是因為那塊區(qū)域跟超類重疊了。唯有蘋果將超類改為以前的布局才能拯救我們,但這樣也導致它們不能再拓展它們的框架了,因為成員變量布局被死死地固定了。在脆弱的實例變量(Fragile ivars) 環(huán)境下我們需要重新編譯繼承自 Apple 的類來恢復兼容性。那么在健壯的實例變量下會發(fā)生什么呢?

在健壯的實例變量下編譯器生成的實例變量布局跟以前一樣,但是當 runtime 系統(tǒng)檢測到與超類有部分重疊時它會調(diào)整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。
需要注意的是在健壯的實例變量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))來代替。
- 我的實驗代碼:
@interface Student ()
{
@private
NSInteger age;
}
@end
@implementation Student
- (NSString *)description
{
NSLog(@"current pointer = %p", self);
NSLog(@"age pointer = %p", &age);
return [NSString stringWithFormat:@"age = %ld", (long)age];
}

上述代碼,Student有個被標記為private的ivar,這個時候當我們使用 .或者-> 訪問時,編譯器會報錯。那么我們?nèi)绾卧O置一個被標記為private的ivar的值呢?
- 通過計算字節(jié)偏量來確定地址
//測試ivar_getOffSet
Student *student = [[Student alloc] init];
Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");
int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));
NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));
*age_pointer = 10;
NSLog(@"%@", student);
-
輸出結(jié)果
off_set
Objective-C Associated Objects
在 OS X 10.6 之后,Runtime系統(tǒng)讓Objc支持向?qū)ο髣討B(tài)添加變量。涉及到的函數(shù)有以下三個:
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );
這些方法以鍵值對的形式動態(tài)地向?qū)ο筇砑印@取或刪除關聯(lián)值。其中關聯(lián)政策是一組枚舉常量:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
這些常量對應著引用關聯(lián)值的政策,也就是 Objc 內(nèi)存管理的引用計數(shù)機制。有關 Objective-C 引用計數(shù)機制的原理,可以查看這篇文章。
Method Swizzling
之前所說的消息轉(zhuǎn)發(fā)雖然功能強大,但需要我們了解并且能更改對應類的源代碼,因為我們需要實現(xiàn)自己的轉(zhuǎn)發(fā)邏輯。當我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現(xiàn)時,該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應的實現(xiàn)來達到“偷天換日”的目的。跟消息轉(zhuǎn)發(fā)相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。
這里摘抄一個 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
上面的代碼通過添加一個Tracking類別到UIViewController類中,將UIViewController
類的viewWillAppear:方法和Tracking類別中xxx_viewWillAppear:方法的實現(xiàn)相互調(diào)換。Swizzling 應該在+load方法中實現(xiàn),因為+load
是在一個類最開始加載時調(diào)用。dispatch_once
是GCD中的一個方法,它保證了代碼塊只執(zhí)行一次,并讓其為一個原子操作,線程安全是很重要的。
如果類中不存在要替換的方法,那就先用class_addMethod和class_replaceMethod函數(shù)添加和替換兩個方法的實現(xiàn);如果類中已經(jīng)有了想要替換的方法,那么就調(diào)用method_exchangeImplementations函數(shù)交換了兩個方法的 IMP,這是蘋果提供給我們用于實現(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,但前者為元類,后者為其本身,因為此時 self 為 Class 而不是實例.注意 [NSObject class] 與 [object class] 的區(qū)別:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
PS:如果類中沒有想被替換實現(xiàn)的原方法時,class_replaceMethod相當于直接調(diào)用class_addMethod向類中添加該方法的實現(xiàn);否則調(diào)用method_setImplementation方法,types
參數(shù)會被忽略。method_exchangeImplementations方法做的事情與如下的原子操作等價:
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),其實不會的。因為[self xxx_viewWillAppear:animated]消息會動態(tài)找到xxx_viewWillAppear:方法的實現(xiàn),而它的實現(xiàn)已經(jīng)被我們與viewWillAppear:
方法實現(xiàn)進行了互換,所以這段代碼不僅不會死循環(huán),如果你把[self xxx_viewWillAppear:animated]換成[self viewWillAppear:animated]
反而會引發(fā)死循環(huán)。
看到有人說+load方法本身就是線程安全的,因為它在程序剛開始就被調(diào)用,很少會碰到并發(fā)問題,于是 stackoverflow 上也有大神給出了另一個 Method Swizzling 的實現(xiàn):
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
NSLog(@"arg1 is %@", arg1);
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}
上面的代碼同樣要添加在某個類的類別中,相比第一個種實現(xiàn),只是去掉了dispatch_once
部分。Method Swizzling 的確是一個值得深入研究的話題,Method Swizzling 的最佳實現(xiàn)是什么呢?小弟才疏學淺理解的不深刻,找了幾篇不錯的資源推薦給大家:
Objective-C的hook方案(一): Method Swizzling
Method Swizzling
How do I implement method swizzling?
What are the Dangers of Method Swizzling in Objective C?
JRSwizzle
- 我的實驗代碼
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// willMoveToSuperview:在一個子視圖將要被添加到另一個視圖的時候發(fā)送此消息
SEL systemSel = @selector(willMoveToSuperview:);
SEL JH_sel = @selector(JH_willMoveToSuperview:);
// class_getInstanceMethod 得到類的實例方法
// class_getClassMethod 得到類的類方法
// Method包含方法名sel和方法實現(xiàn)IMP
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method JH_method = class_getInstanceMethod([self class], JH_sel);
// class_addMethod 為類添加新方法,放回yes可以添加,放回no說明已存在該方法
// method_getTypeEncoding 獲取方法的Type字符串(包含參數(shù)類型和返回值類型)
// 為當前類添加一個方法,名字sel為systemSel,方法實現(xiàn)為JH_method
// [UIStatusBarBackgroundView myWillMoveToSuperview:]: unrecognized selector sent to instance 0x7ff2176020b0
if (class_addMethod([self class], systemSel, method_getImplementation(JH_method), method_getTypeEncoding(JH_method))) {
// 添加成功,說明此類不包含該方法
class_replaceMethod([self class], JH_sel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}else{
// 添加失敗,說明存在此方法,直接交換即可
method_exchangeImplementations(systemMethod, JH_method);
}
});
}
#pragma mark 自己實現(xiàn)的一個新方法
- (void)JH_willMoveToSuperview:(UIView *)superView
{
//此處調(diào)用的是系統(tǒng)的方法
[self JH_willMoveToSuperview:superView];
if ([self isKindOfClass:NSClassFromString(@"UIButtonLabel")]) {
//按鈕的樣式不改變
return;
}
if ([self isKindOfClass:[UILabel class]]) {
UILabel *lable = (UILabel *)self;
//為特殊的label打上特定的標簽
if (self.tag == 10000) {
lable.textColor = [UIColor blueColor];
}else{
lable.textColor = [UIColor redColor];
}
}
}
- 這里如果直接交換會出問題:直接交換 IMP 是很危險的。因為如果這個類中沒有實現(xiàn)這個方法,class_getInstanceMethod() 返回的是某個父類的 Method 對象,這樣 method_exchangeImplementations() 就把父類的原始實現(xiàn)(IMP)跟這個類的 Swizzle 實現(xiàn)交換了。這樣其他父類及其其他子類的方法調(diào)用就會出問題,最嚴重的就是 Crash。
- 附上這部分的項目地址
在用 SpriteKit 寫游戲的時候,因為 API 本身有一些缺陷(增刪節(jié)點時不考慮父節(jié)點是否存在啊,很容易崩潰啊有木有!),我在 Swift 上使用 Method Swizzling彌補這個缺陷:
extension SKNode {
class func yxy_swizzleAddChild() {
let cls = SKNode.self
let originalSelector = Selector("addChild:")
let swizzledSelector = Selector("yxy_addChild:")
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
class func yxy_swizzleRemoveFromParent() {
let cls = SKNode.self
let originalSelector = Selector("removeFromParent")
let swizzledSelector = Selector("yxy_removeFromParent")
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
func yxy_addChild(node: SKNode) {
if node.parent == nil {
self.yxy_addChild(node)
}
else {
println("This node has already a parent!\(node.name)")
}
}
func yxy_removeFromParent() {
if parent != nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.yxy_removeFromParent()
})
}
else {
println("This node has no parent!\(name)")
}
}
}
然后其他地方調(diào)用那兩個類方法:
SKNode.yxy_swizzleAddChild()
SKNode.yxy_swizzleRemoveFromParent()
因為 Swift 中的 extension 的特殊性,最好在某個類的load() 方法中調(diào)用上面的兩個方法.我是在AppDelegate 中調(diào)用的,于是保證了應用啟動時能夠執(zhí)行上面兩個方法.
總結(jié)
我們之所以讓自己的類繼承NSObject
不僅僅因為蘋果幫我們完成了復雜的內(nèi)存分配問題,更是因為這使得我們能夠用上 Runtime 系統(tǒng)帶來的便利。可能我們平時寫代碼時可能很少會考慮一句簡單的[receiver message]
背后發(fā)生了什么,而只是當做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細節(jié)更有利于我們利用消息機制寫出功能更強大的代碼,比如 Method Swizzling 等。
參考鏈接:
Objective-C Runtime Programming Guide
Objective-C runtime之運行時的基本特點
Understanding the Objective-C Runtime
http://www.tuicool.com/articles/FjqAfa












