字數5000+,預計閱讀時間 30分鐘
複製程式碼
主要參考自:
執行時簡介
Objective-C
語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。
對於Objective-C
來說,這個執行時系統就像一個作業系統一樣:它讓所有的工作可以正常的執行。Runtime
基本上是用C
和彙編
寫的,這個庫使得C語言
有了物件導向的能力。
在Runtime
中,物件可以用C語言
中的結構體表示,而方法可以用C
函式來實現,另外再加上了一些額外的特性。這些結構體和函式被runtime函式封裝後,讓OC
的物件導向程式設計變為可能。
找出方法的最終執行程式碼:當程式執行[object doSomething]
時,會向訊息接收者(object)
傳送一條訊息(doSomething)
,Runtime
會根據訊息接收者是否能響應該訊息而做出不同的反應。
一、類與物件基礎資料結構
1、object_class 類
Objective-C
類是由Class型別來表示的,它實際上是一個指
向objc_class
結構體的指標。
typedef struct object_class *Class
複製程式碼
它的定義如下:
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本資訊,預設為0
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;
複製程式碼
2、objc_object 例項
objc_object
是表示一個類的例項的結構體
它的定義如下:
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
複製程式碼
可以看到,這個結構體只有一個字型,即指向其類的isa
指標。這
樣,當我們向一個Objective-C
物件傳送訊息時,執行時庫會根據
例項物件的isa指標找到這個例項物件所屬的類。Runtime
庫會在類
的方法列表及父類的方法列表中去尋找與訊息對應的selector
指向
的方法,找到後即執行這個方法。
3、元類(Meta Class)
meta-class
是一個類物件的類。
在上面我們提到,所有的類自身也是一個物件,我們可以向這個物件傳送訊息(即呼叫類方法)。
既然是物件,那麼它也是一個objc_object
指標,它包含一個指向其類的一個isa
指標。那麼,這個isa
指標指向什麼呢?
答案是,為了呼叫類方法,這個類的isa
指標必須指向一個包含這些類方法的一個objc_class
結構體。這就引出了meta-class
的概念,meta-class
中儲存著一個類的所有類方法。
所以,呼叫類方法的這個類物件的isa
指標指向的就是meta-class
當我們向一個物件傳送訊息時,runtime
會在這個物件所屬的這個類的方法列表中查詢方法;而向一個類傳送訊息時,會在這個類的meta-class
的方法列表中查詢。
再深入一下,meta-class
也是一個類,也可以向它傳送一個訊息,那麼它的isa
又是指向什麼呢?為了不讓這種結構無限延伸下去,Objective-C
的設計者讓所有的meta-class
的isa
指向基類的meta-class
,以此作為它們的所屬類。
即,任何NSObject
繼承體系下的meta-class
都使用NSObject
的meta-class
作為自己的所屬類,而基類的meta-class
的isa
指標是指向它自己。
通過上面的描述,再加上對objc_class
結構體中super_class
指標的分析,我們就可以描繪出類及相應meta-class
類的一個繼承體系了,如下:
4、Category
Category
是表示一個指向分類的結構體的指標,其定義如下:
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; // 例項方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 類方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分類所實現的協議列表
}
複製程式碼
這個結構體主要包含了分類定義的例項方法與類方法,其中instance_methods
列表是objc_class
中方法列表的一個子集,而class_methods
列表是元類方法列表的一個子集。
可發現,類別中沒有ivar
成員變數指標,也就意味著:類別中不能夠新增例項變數和屬性,(!除非使用關聯物件
,而且Category
中的屬性,只會生成setter和getter方法,不會生成成員變數)例子如下
#import "UIButton+ClickBlock.h"
#import static const void *associatedKey = "associatedKey";
@implementation UIButton (ClickBlock)
//Category中的屬性,只會生成setter和getter方法,不會生成成員變數
-(void)setClick:(clickBlock)click{
objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
if (click) {
[self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}
}
-(clickBlock)click{
return objc_getAssociatedObject(self, associatedKey);
}
-(void)buttonClick{
if (self.click) {
self.click();
}
}
@end
複製程式碼
然後在程式碼中,就可以使用 UIButton
的屬性來監聽單擊事件了:
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = self.view.bounds;
[self.view addSubview:button];
button.click = ^{
NSLog(@"buttonClicked");
};
複製程式碼
關聯物件的使用
1.設定關聯值
引數說明:
object:與誰關聯,通常是傳self
key:唯一鍵,在獲取值時通過該鍵獲取,通常是使用static
const void *來宣告
value:關聯所設定的值
policy:記憶體管理策略,比如使用copy
void objc_setAssociatedObject(id object, const void *key, id value, objc _AssociationPolicy policy)
複製程式碼
2.獲取關聯值
引數說明:
object:與誰關聯,通常是傳self,在設定關聯時所指定的與哪個物件關聯的那個物件
key:唯一鍵,在設定關聯時所指定的鍵
id objc_getAssociatedObject(id object, const void *key)
複製程式碼
3.取消關聯
void objc_removeAssociatedObjects(id object)
複製程式碼
關聯策略
使用場景:
可以在類別中新增屬性
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0, // 表示弱引用關聯,通常是基本資料型別
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 表示強引用關聯物件,是執行緒安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 表示關聯物件copy,是執行緒安全的
OBJC_ASSOCIATION_RETAIN = 01401, // 表示強引用關聯物件,不是執行緒安全的
OBJC_ASSOCIATION_COPY = 01403 // 表示關聯物件copy,不是執行緒安全的
};
複製程式碼
二、方法與訊息
1、SEL
SEL又叫選擇器,是表示一個方法的selector
的指標,其定義如下:
typedef struct objc_selector *SEL;
複製程式碼
方法的selector
用於表示執行時方法的名字。Objective-C
在編譯時,會依據每一個方法的名字、引數序列,生成一個唯一的整型標識(Int型別的地址)
,這個標識就是SEL
。
兩個類之間,只要方法名相同,那麼方法的SEL
就是一樣的,每一個方法都對應著一個SEL
。所以在Objective-C
同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使引數型別不同也不行
如在某一個類中定義以下兩個方法: 錯誤
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
複製程式碼
當然,不同的類可以擁有相同的selector
,這個沒有問題。不同類的例項物件執行相同的selector
時,會在各自的方法列表中去根據selector
去尋找自己對應的IMP
。
工程中的所有的SEL
組成一個Set
集合,如果我們想到這個方法集合中查詢某個方法時,只需要去找到這個方法對應的SEL
就行了,SEL
實際上就是根據方法名hash
化了的一個字串,而對於字串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比!
本質上,SEL
只是一個指向方法的指標(準確的說,只是一個根據方法名hash
化了的KEY
值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。
通過下面三種方法可以獲取SEL:
a、sel_registerName函式
b、Objective-C編譯器提供的@selector()
c、NSSelectorFromString()方法
複製程式碼
2、IMP
IMP
實際上是一個函式指標,指向方法實現的地址。
其定義如下:
id (*IMP)(id, SEL,...)
複製程式碼
第一個引數:是指向self
的指標(如果是例項方法,則是類例項的記憶體地址;如果是類方法,則是指向元類的指標)
第二個引數:是方法選擇器(selector)
接下來的引數:方法的引數列表。
前面介紹過的SEL
就是為了查詢方法的最終實現IMP
的。由於每個方法對應唯一的SEL
,因此我們可以通過SEL
方便快速準確地獲得它所對應的IMP
,查詢過程將在下面討論。取得IMP
後,我們就獲得了執行這個方法程式碼的入口點,此時,我們就可以像呼叫普通的C語言
函式一樣來使用這個函式指標了。
3、Method
Method用於表示類定義中的方法,則定義如下:
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法實現
}
複製程式碼
我們可以看到該結構體中包含一個SEL
和IMP
,實際上相當於在SEL
和IMP
之間作了一個對映。有了SEL
,我們便可以找到對應的IMP
,從而呼叫方法的實現程式碼。
4、訊息
Objc 中傳送訊息是用中括號([]
)把接收者和訊息括起來,而直到執行時才會把訊息與方法實現繫結。
有關訊息傳送和訊息轉發機制的原理,可以檢視這篇文章。
面對著 Cocoa
中大量 API
,只知道簡單的查文件和呼叫。還記得初學 Objective-C
時把 [receiver message]
當成簡單的方法呼叫,而無視了“傳送訊息”這句話的深刻含義。其實 [receiver message]
會被編譯器轉化為:
objc_msgSend(receiver, selector)
複製程式碼
如果訊息含有引數,則為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
複製程式碼
如果訊息的接收者能夠找到對應的 selector
,那麼就相當於直接執行了接收者這個物件的特定方法;否則,訊息要麼被轉發,或是臨時向接收者動態新增這個 selector
對應的實現內容,要麼就乾脆玩完崩潰掉。
現在可以看出[receiver message]
真的不是一個簡簡單單的方法呼叫。因為這只是在編譯階段確定了要向接收者傳送 message
這條訊息,而 receiver
將要如何響應這條訊息,那就要看執行時發生的情況來決定了。
這裡看起來像是objc_msgSend
返回了資料,其實objc_msgSend
從不返回資料而是你的方法被呼叫後返回了資料。下面詳細敘述下訊息傳送步驟:
1、檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函式了。
2、檢測這個 target 是不是 nil 物件。ObjC 的特性是允許對一個 nil 物件執行任何一個方法不會 Crash,因為會被忽略掉。
3、如果上面兩個都過了,那就開始查詢這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函式去執行。
4、如果 cache 找不到就找一下方法分發表。
5、如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
6、如果還找不到就要開始進入動態方法解析了,後面會提到。
PS:這裡說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯絡起來。
複製程式碼
其實編譯器會根據情況在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四個方法中選擇一個來呼叫。如果訊息是傳遞給超類,那麼會呼叫名字帶有”Super”
的函式;如果訊息返回值是資料結構而不是簡單值時,那麼會呼叫名字帶有”stret”
的函式。排列組合正好四個方法。
值得一提的是在i386
平臺處理返回型別為浮點數的訊息時,需要用到objc_msgSend_fpret
函式來進行處理,這是因為返回型別為浮點數的函式對應的ABI(Application Binary Interface)
與返回整型的函式的ABI
不相容。此時objc_msgSend
不再適用,於是objc_msgSend_fpret
被派上用場,它會對浮點數暫存器做特殊處理。不過在PPC
或 PPC64
平臺是不需要麻煩它的。
PS:有木有發現這些函式的命名規律哦?帶“Super”
的是訊息傳遞給超類;“stret”
可分為“st”+“ret”
兩部分,分別代表“struct”
和“return”
;“fpret”
就是“fp”+“ret”
,分別代表“floating-point”
和“return”
。
5、動態方法解析
你可以動態地提供一個方法的實現。例如我們可以用@dynamic
關鍵字在類的實現檔案中修飾一個屬性:
@dynamic propertyName;
複製程式碼
這表明我們會為這個屬性動態提供存取方法,也就是說編譯器不會再預設為我們生成setPropertyName:
和propertyName
方法,而需要我們動態提供。我們可以通過分別過載resolveInstanceMethod:
和resolveClassMethod:
方法分別新增例項方法實現和類方法實現。因為當Runtime
系統在Cache
和方法分發表中(包括超類)找不到要執行的方法時,Runtime
會呼叫resolveInstanceMethod:
或resolveClassMethod:
來給程式設計師一次動態新增方法實現的機會。我們需要用class_addMethod
函式完成向特定類新增特定方法實現的操作:
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
方法新增了實現內容,也就是dynamicMethodIMP
方法中的程式碼。其中 “v@:
” 表示返回值和引數,這個符號涉及 Type Encoding
PS:動態方法解析會在訊息轉發機制浸入前執行。如果 respondsToSelector:
或 instancesRespondToSelector:
方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP
的機會。如果你想讓該方法選擇器被傳送到轉發機制,那麼就讓resolveInstanceMethod:
返回NO
。
備註: 解析類方法等具體做法 例: .h
#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])
的關係,其實並不難,重點在於 self
的型別:
1、當 self 為例項物件時,[self class] 與 object_getClass(self) 等價,因為前者會呼叫後者。
object_getClass([self class]) 得到元類。
2、當 self 為類物件時,[self class] 返回值為自身,還是 self。
object_getClass(self) 與 object_getClass([self class]) 等價。
複製程式碼
6、訊息轉發
重定向
在訊息轉發機制執行前,Runtime
系統會再給我們一次偷樑換柱的機會,即通過過載- (id)forwardingTargetForSelector:(SEL)aSelector
方法替換訊息的接受者為其他物件:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
複製程式碼
畢竟訊息轉發要耗費更多時間,抓住這次機會將訊息重定向給別人是個不錯的選擇,如果此方法返回nil
或self
,則會進入訊息轉發機制(forwardInvocation:);
否則將向返回的物件重新傳送訊息。
如果想替換類方法的接受者,需要覆寫 + (id)forwardingTargetForSelector:(SEL)aSelector
方法,並返回類物件:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
複製程式碼
轉發
當動態方法解析不作處理返回NO
時,訊息轉發機制會被觸發。在這時forwardInvocation:
方法會被執行,我們可以重寫這個方法來定義我們的轉發邏輯:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
複製程式碼
該訊息的唯一引數是個NSInvocation
型別的物件——該物件封裝了原始的訊息和訊息的引數。我們可以實現forwardInvocation:
方法來對不能處理的訊息做一些預設的處理,也可以將訊息轉發給其他物件來處理,而不丟擲錯誤。
這裡需要注意的是引數anInvocation
是從哪的來的呢?其實在forwardInvocation:
訊息傳送前,Runtime
系統會向物件傳送methodSignatureForSelector:
訊息,並取到返回的方法簽名用於生成NSInvocation
物件。所以我們在重寫forwardInvocation:
的同時也要重寫methodSignatureForSelector:
方法,否則會拋異常。
當一個物件由於沒有相應的方法實現而無法響應某訊息時,執行時系統將通過forwardInvocation:
訊息通知該物件。每個物件都從NSObject
類中繼承了forwardInvocation:
方法。然而,NSObject
中的方法實現只是簡單地呼叫了doesNotRecognizeSelector:
。通過實現我們自己的forwardInvocation:
方法,我們可以在該方法實現中將訊息轉發給其它物件。
forwardInvocation:
方法就像一個不能識別的訊息的分發中心,將這些訊息轉發給不同接收物件。或者它也可以象一個運輸站將所有的訊息都傳送給同一個接收物件。它可以將一個訊息翻譯成另外一個訊息,或者簡單的”吃掉“某些訊息,因此沒有響應也沒有錯誤。forwardInvocation:
方法也可以對不同的訊息提供同樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不同的物件連結到訊息鏈的能力。
注意: forwardInvocation:
方法只有在訊息接收物件中無法正常響應訊息時才會被呼叫。 所以,如果我們希望一個物件將negotiate
訊息轉發給其它物件,則這個物件不能有negotiate
方法。否則,forwardInvocation:
將不可能會被呼叫。
轉發與多繼承
轉發和繼承相似,可以用於為Objc程式設計新增一些多繼承的效果。就像下圖那樣,一個物件把訊息轉發出去,就好似它把另一個物件中的方法借過來或是“繼承”過來一樣。
這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中Warrior
和Diplomat
沒有繼承關係,但是Warrior
將negotiate
訊息轉發給了Diplomat
後,就好似Diplomat
是Warrior
的超類一樣。
訊息轉發彌補了 Objc 不支援多繼承的性質,也避免了因為多繼承導致單個類變得臃腫複雜。它將問題分解得很細,只針對想要借鑑的方法才轉發,而且轉發機制是透明的。
轉發與繼承
儘管轉發很像繼承,但是NSObject
類不會將兩者混淆。像respondsToSelector:
和 isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉發鏈。比如上圖中一個Warrior
物件如果被問到是否能響應negotiate
訊息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
複製程式碼
結果是NO
,儘管它能夠接受negotiate
訊息而不報錯,因為它靠轉發訊息給Diplomat
類來響應訊息。
如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior
繼承到了Diplomat
的negotiate
方法,你得重新實現 respondsToSelector:
和 isKindOfClass:
來加入你的轉發演算法:
- (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:
中也應該寫一份轉發演算法。如果使用了協議,conformsToProtocol:
同樣也要加入到這一行列中。類似地,如果一個物件轉發它接受的任何遠端訊息,它得給出一個methodSignatureForSelector:
來返回準確的方法描述,這個方法會最終響應被轉發的訊息。比如一個物件能給它的替代者物件轉發訊息,它需要像下面這樣實現methodSignatureForSelector::
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
複製程式碼
7、方法交換 Method Swizzling
之前所說的訊息轉發雖然功能強大,但需要我們瞭解並且能更改對應類的原始碼,因為我們需要實現自己的轉發邏輯。當我們無法觸碰到某個類的原始碼,卻想更改這個類某個方法的實現時,該怎麼辦呢?可能繼承類並重寫方法是一種想法,但是有時無法達到目的。這裡介紹的是 Method Swizzling
,它通過重新對映方法對應的實現來達到“偷天換日”的目的。跟訊息轉發相比,Method Swizzling
的做法更為隱蔽,甚至有些冒險,也增大了debug
的難度。
Swizzling原理
在Objective-C中呼叫一個方法,其實是向一個物件傳送訊息,而查詢訊息的唯一依據是selector的名字。
所以,我們可以利用Objective-C的runtime機制,實現在執行時交換selector對應的方法實現以達到我們的目的。
每個類都有一個方法列表,存放著selector的名字和方法實現的對映關係。
IMP有點類似函式指標,指向具體的Method實現
複製程式碼
這是參考Mattt大神在NSHipster上的文章自己寫的程式碼。
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(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:
方法的實現相互調換。Swizzling
應該在+load
方法中實現,因為+load
是在一個類最開始載入時呼叫。dispatch_once
是GCD中的一個方法,它保證了程式碼塊只執行一次,並讓其為一個原子操作,執行緒安全是很重要的。
如果類中不存在要替換的方法,那就先用class_addMethod
和class_replaceMethod
函式新增和替換兩個方法的實現;如果類中已經有了想要替換的方法,那麼就呼叫method_exchangeImplementations
函式交換了兩個方法的 IMP
,這是蘋果提供給我們用於實現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]
返回的結果型別都是 Class
,但前者為元類,後者為其本身,因為此時 self
為 Class
而不是例項.注意 [NSObject class]
與 [object class]
的區別:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
複製程式碼
PS: 如果類中沒有想被替換實現的原方法時,class_replaceMethod
相當於直接呼叫class_addMethod
向類中新增該方法的實現;否則呼叫method_setImplementation
方法,types
引數會被忽略。method_exchangeImplementations
方法做的事情與如下的原子操作等價:
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
複製程式碼
最後xxx_viewWillAppear:
方法的定義看似是遞迴呼叫引發死迴圈,其實不會的。因為[self xxx_viewWillAppear:animated]
訊息會動態找到xxx_viewWillAppear:
方法的實現,而它的實現已經被我們與viewWillAppear:
方法實現進行了互換,所以這段程式碼不僅不會死迴圈,如果你把[self xxx_viewWillAppear:animated]
換成[self viewWillAppear:animated]
反而會引發死迴圈。
PS: 看到有人說+load方法本身就是執行緒安全的,因為它在程式剛開始就被呼叫,很少會碰到併發問題,於是 stackoverflow 上也有大神去掉了dispatch_once 部分。
擴充套件閱讀: 1、iOS---防止UIButton重複點選的三種實現方式 2、Swift Runtime分析:還像OC Runtime一樣嗎?