iOS Runtime的理解

字數(shù)5000+,預計閱讀時間 30分鐘

主要參考自:
1、iOS運行時(Runtime)詳解+Demo
2、Objective-C Runtime
3、神經(jīng)病院Objective-C Runtime出院第三天——如何正確使用Runtime

運行時簡介

Objective-C語言是一門動態(tài)語言,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了運行時來處理。
對于Objective-C來說,這個運行時系統(tǒng)就像一個操作系統(tǒng)一樣:它讓所有的工作可以正常的運行。Runtime基本上是用C匯編寫的,這個庫使得C語言有了面向?qū)ο蟮哪芰Α?br> 在Runtime中,對象可以用C語言中的結(jié)構(gòu)體表示,而方法可以用C函數(shù)來實現(xiàn),另外再加上了一些額外的特性。這些結(jié)構(gòu)體和函數(shù)被runtime函數(shù)封裝后,讓OC的面向?qū)ο缶幊套優(yōu)榭赡堋?br> 找出方法的最終執(zhí)行代碼:當程序執(zhí)行[object doSomething]時,會向消息接收者(object)發(fā)送一條消息(doSomething)Runtime會根據(jù)消息接收者是否能響應該消息而做出不同的反應。

一、類與對象基礎數(shù)據(jù)結(jié)構(gòu)

1、object_class 類
Objective-C類是由Class類型來表示的,它實際上是一個指
objc_class結(jié)構(gòu)體的指針。

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;  // 協(xié)議鏈表
#endif
}OBJC2_UNAVAILABLE;

2、objc_object 實例
objc_object是表示一個類的實例的結(jié)構(gòu)體
它的定義如下:

struct objc_object{
     Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可以看到,這個結(jié)構(gòu)體只有一個字體,即指向其類的isa指針。這
樣,當我們向一個Objective-C對象發(fā)送消息時,運行時庫會根據(jù)
實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類
的方法列表及父類的方法列表中去尋找與消息對應的selector指向
的方法,找到后即運行這個方法。

3、元類(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
當我們向一個對象發(fā)送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發(fā)送消息時,會在這個類的meta-class的方法列表中查找。

再深入一下,meta-class也是一個類,也可以向它發(fā)送一個消息,那么它的isa又是指向什么呢?為了不讓這種結(jié)構(gòu)無限延伸下去,Objective-C的設計者讓所有的meta-classisa指向基類的meta-class,以此作為它們的所屬類。

即,任何NSObject繼承體系下的meta-class都使用NSObjectmeta-class作為自己的所屬類,而基類的meta-classisa指針是指向它自己。

通過上面的描述,再加上對objc_class結(jié)構(gòu)體中super_class指針的分析,我們就可以描繪出類及相應meta-class類的一個繼承體系了,如下:

OC 中的繼承體系.png

4、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;  // 實例方法列表
     struct objc_method_list *class_methods      OBJC2_UNAVAILABLE; // 類方法列表
     struct objc_protocol_list *protocols        OBJC2_UNAVAILABLE; // 分類所實現(xiàn)的協(xié)議列表
}

這個結(jié)構(gòu)體主要包含了分類定義的實例方法與類方法,其中instance_methods列表是objc_class中方法列表的一個子集,而class_methods列表是元類方法列表的一個子集。
可發(fā)現(xiàn),類別中沒有ivar成員變量指針,也就意味著:類別中不能夠添加實例變量和屬性,(!除非使用關(guān)聯(lián)對象,而且Category中的屬性,只會生成setter和getter方法,不會生成成員變量)例子如下

#import "UIButton+ClickBlock.h"
#import static const void *associatedKey = "associatedKey";
@implementation UIButton (ClickBlock)
//利用關(guān)聯(lián)對象在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 的屬性來監(jiān)聽單擊事件了:

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = self.view.bounds;
[self.view addSubview:button];
button.click = ^{
    NSLog(@"buttonClicked");
};

關(guān)聯(lián)對象的使用

1.設置關(guān)聯(lián)值
參數(shù)說明:
object:與誰關(guān)聯(lián),通常是傳self
key:唯一鍵,在獲取值時通過該鍵獲取,通常是使用static
const void *來聲明
value:關(guān)聯(lián)所設置的值
policy:內(nèi)存管理策略,比如使用copy

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,在設置關(guān)聯(lián)時所指定的與哪個對象關(guān)聯(lián)的那個對象
key:唯一鍵,在設置關(guān)聯(lián)時所指定的鍵
id objc_getAssociatedObject(id object, const void *key)
3.取消關(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ù)類型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示強引用關(guān)聯(lián)對象,是線程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示關(guān)聯(lián)對象copy,是線程安全的
OBJC_ASSOCIATION_RETAIN = 01401,         // 表示強引用關(guān)聯(lián)對象,不是線程安全的
OBJC_ASSOCIATION_COPY = 01403            // 表示關(guān)聯(lián)對象copy,不是線程安全的
};

二、方法與消息

1、SEL
SEL又叫選擇器,是表示一個方法的selector的指針,其定義如下:

typedef struct objc_selector *SEL;

方法的selector用于表示運行時方法的名字。Objective-C在編譯時,會依據(jù)每一個方法的名字、參數(shù)序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL
兩個類之間,只要方法名相同,那么方法的SEL就是一樣的,每一個方法都對應著一個SEL。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數(shù)類型不同也不行
如在某一個類中定義以下兩個方法: 錯誤

- (void)setWidth:(int)width;
- (void)setWidth:(double)width;

當然,不同的類可以擁有相同的selector,這個沒有問題。不同類的實例對象執(zhí)行相同的selector時,會在各自的方法列表中去根據(jù)selector去尋找自己對應的IMP。
工程中的所有的SEL組成一個Set集合,如果我們想到這個方法集合中查找某個方法時,只需要去找到這個方法對應的SEL就行了,SEL實際上就是根據(jù)方法名hash化了的一個字符串,而對于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比!
本質(zhì)上,SEL只是一個指向方法的指針(準確的說,只是一個根據(jù)方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。

通過下面三種方法可以獲取SEL:
a、sel_registerName函數(shù)
b、Objective-C編譯器提供的@selector()
c、NSSelectorFromString()方法

2、IMP
IMP實際上是一個函數(shù)指針,指向方法實現(xiàn)的地址。
其定義如下:

id (*IMP)(id, SEL,...)

第一個參數(shù):是指向self的指針(如果是實例方法,則是類實例的內(nèi)存地址;如果是類方法,則是指向元類的指針)
第二個參數(shù):是方法選擇器(selector)
接下來的參數(shù):方法的參數(shù)列表。

前面介紹過的SEL就是為了查找方法的最終實現(xiàn)IMP的。由于每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的IMP,查找過程將在下面討論。取得IMP后,我們就獲得了執(zhí)行這個方法代碼的入口點,此時,我們就可以像調(diào)用普通的C語言函數(shù)一樣來使用這個函數(shù)指針了。

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; // 方法實現(xiàn)
}

我們可以看到該結(jié)構(gòu)體中包含一個SELIMP,實際上相當于在SELIMP之間作了一個映射。有了SEL,我們便可以找到對應的IMP,從而調(diào)用方法的實現(xiàn)代碼。

4、消息
Objc 中發(fā)送消息是用中括號([])把接收者和消息括起來,而直到運行時才會把消息與方法實現(xiàn)綁定。

有關(guān)消息發(fā)送和消息轉(zhuǎn)發(fā)機制的原理,可以查看這篇文章


面對著 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這條消息,而 receiver 將要如何響應這條消息,那就要看運行時發(fā)生的情況來決定了。

這里看起來像是objc_msgSend返回了數(shù)據(jù),其實objc_msgSend從不返回數(shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細敘述下消息發(fā)送步驟:

1、檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain, release 這些函數(shù)了。
2、檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉。
3、如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數(shù)去執(zhí)行。
4、如果 cache 找不到就找一下方法分發(fā)表。
5、如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
6、如果還找不到就要開始進入動態(tài)方法解析了,后面會提到。
PS:這里說的分發(fā)表其實就是Class中的方法列表,它將方法選擇器和方法實現(xiàn)地址聯(lián)系起來。
方法調(diào)用流程.gif

其實編譯器會根據(jù)情況在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四個方法中選擇一個來調(diào)用。如果消息是傳遞給超類,那么會調(diào)用名字帶有”Super”的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時,那么會調(diào)用名字帶有”stret”的函數(shù)。排列組合正好四個方法。

值得一提的是在i386 平臺處理返回類型為浮點數(shù)的消息時,需要用到objc_msgSend_fpret函數(shù)來進行處理,這是因為返回類型為浮點數(shù)的函數(shù)對應的ABI(Application Binary Interface)與返回整型的函數(shù)的ABI不兼容。此時objc_msgSend不再適用,于是objc_msgSend_fpret被派上用場,它會對浮點數(shù)寄存器做特殊處理。不過在PPCPPC64 平臺是不需要麻煩它的。

PS:有木有發(fā)現(xiàn)這些函數(shù)的命名規(guī)律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”“return”

5、動態(tài)方法解析
你可以動態(tài)地提供一個方法的實現(xiàn)。例如我們可以用@dynamic關(guān)鍵字在類的實現(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

備注:解析類方法等具體做法 例:
.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]) 的關(guān)系,其實并不難,重點在于 self 的類型:

1、當 self 為實例對象時,[self class] 與 object_getClass(self) 等價,因為前者會調(diào)用后者。
object_getClass([self class]) 得到元類。

2、當 self 為類對象時,[self class] 返回值為自身,還是 self。
object_getClass(self) 與 object_getClass([self class]) 等價。

6、消息轉(zhuǎn)發(fā)

消息轉(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ā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,如果此方法返回nilself,則會進入消息轉(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ā)出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。

forwarding.gif

這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中WarriorDiplomat沒有繼承關(guān)系,但是Warriornegotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似DiplomatWarrior的超類一樣。

消息轉(zhuǎn)發(fā)彌補了 Objc 不支持多繼承的性質(zhì),也避免了因為多繼承導致單個類變得臃腫復雜。它將問題分解得很細,只針對想要借鑒的方法才轉(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繼承到了Diplomatnegotiate方法,你得重新實現(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;
}

7、方法交換 Method Swizzling
之前所說的消息轉(zhuǎn)發(fā)雖然功能強大,但需要我們了解并且能更改對應類的源代碼,因為我們需要實現(xiàn)自己的轉(zhuǎn)發(fā)邏輯。當我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現(xiàn)時,該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應的實現(xiàn)來達到“偷天換日”的目的。跟消息轉(zhuǎn)發(fā)相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。

Swizzling原理
在Objective-C中調(diào)用一個方法,其實是向一個對象發(fā)送消息,而查找消息的唯一依據(jù)是selector的名字。
所以,我們可以利用Objective-C的runtime機制,實現(xiàn)在運行時交換selector對應的方法實現(xiàn)以達到我們的目的。

每個類都有一個方法列表,存放著selector的名字和方法實現(xiàn)的映射關(guān)系。
IMP有點類似函數(shù)指針,指向具體的Method實現(xiàn)

這是參考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:方法的實現(xiàn)相互調(diào)換。Swizzling 應該在+load方法中實現(xiàn),因為+load是在一個類最開始加載時調(diào)用。dispatch_once是GCD中的一個方法,它保證了代碼塊只執(zhí)行一次,并讓其為一個原子操作,線程安全是很重要的。

如果類中不存在要替換的方法,那就先用class_addMethodclass_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,但前者為元類,后者為其本身,因為此時 selfClass 而不是實例.注意 [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)。
PS:看到有人說+load方法本身就是線程安全的,因為它在程序剛開始就被調(diào)用,很少會碰到并發(fā)問題,于是 stackoverflow 上也有大神去掉了dispatch_once 部分。

擴展閱讀:
1、iOS---防止UIButton重復點擊的三種實現(xiàn)方式
2、Swift Runtime分析:還像OC Runtime一樣嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,101評論 0 9
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 892評論 0 1
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,351評論 0 7
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,252評論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 842評論 0 2

友情鏈接更多精彩內(nèi)容