iOS開發(fā)之進(jìn)階篇(9)—— runtime運(yùn)行時(shí)

目錄

  • 前言
  • iOS編譯流程
  • runtime介紹
  • 消息發(fā)送流程
  • 消息轉(zhuǎn)發(fā)流程
  • Method Swizzling
  • 參考文檔

前言

關(guān)于runtime的文章, 網(wǎng)上實(shí)在太多了, 內(nèi)容層次深淺不一. 誠然, 要想把runtime討論明白, 講得深入徹底, 沒有相當(dāng)功力是不行的. 故, 本文退而求其次, 希望能把runtime講得"知其然", 想必也是挺好的.

iOS編譯流程

我們編寫的所有代碼, 最終都是要轉(zhuǎn)換成二進(jìn)制機(jī)器指令去執(zhí)行的. 如圖

C/C++/OC編譯流程:

clang@2x.png

Swift編譯流程:

swiftc@2x.png

:
例圖是以模擬器為例, 所以編譯結(jié)果為 x86-64 CPU 機(jī)器語言; 如果是真機(jī), 則最終編譯成 ARM CPU 機(jī)器語言

解析

  • iOS 開發(fā)中 Objective-C 是 Clang / LLVM 來編譯的。
  • Swift 是 Swift / LLVM惹挟,其中 Swift 前端會多出 SIL optimizer垄惧,它會把 .swift 生成的中間代碼 .sil 屬于 High-Level IR, 因?yàn)?swift 在編譯時(shí)就完成了方法綁定直接通過地址調(diào)用屬于強(qiáng)類型語言嘹害,方法調(diào)用不再是像OC那樣的消息發(fā)送撮竿,這樣編譯就可以獲得更多的信息用在后面的后端優(yōu)化上。
  • 不管編譯的語言時(shí) Objective-C 還是 Swift 也不管對應(yīng)機(jī)器是什么笔呀,亦或是即時(shí)編譯幢踏,LLVM 里唯一不變的是中間語言 LLVM IR。

引申
高級編程語言想要成為可執(zhí)行文件需要先編譯為匯編語言再匯編為機(jī)器語言许师,但是OC并不能直接編譯為匯編語言房蝉,而是要先轉(zhuǎn)寫為純C語言再進(jìn)行編譯和匯編的操作,從OC到C語言的過渡就是由runtime來實(shí)現(xiàn)的微渠。

關(guān)于iOS中的匯編, 詳見深入iOS系統(tǒng)底層之匯編語言.

結(jié)論
在整個(gè)iOS編譯過程中, runtime處在LLVM的前端部分(Frontend). 更具體點(diǎn), 可以說是runtime將OC轉(zhuǎn)換成C.

runtime介紹

為何要有runtime

  • C: 靜態(tài)語言. 編譯階段就要決定調(diào)用哪個(gè)函數(shù), 如果函數(shù)未實(shí)現(xiàn)就會編譯報(bào)錯(cuò).
  • OC: 動態(tài)語言(得益于runtime機(jī)制). 運(yùn)行時(shí)才決定調(diào)用哪個(gè)函數(shù), 只要函數(shù)聲明過即使沒有實(shí)現(xiàn)也不會報(bào)錯(cuò).
  • Swift: 靜態(tài)語言. 其對象方法的調(diào)用基本上是在編譯鏈接時(shí)刻就被確定的. 詳見Swift5.0的Runtime機(jī)制淺析.

Swift基本上取消了runtime機(jī)制, 故本文還是主要討論OC下的runtime. 當(dāng)然, 通過Swift與OC混編, 我們也可以在Swift文件中調(diào)用OC的runtime接口.

總所周知, OC 擴(kuò)展自 C 語言搭幻,然后擁有了面向?qū)ο笮再|(zhì)和消息傳遞機(jī)制, 成為了動態(tài)語言。而這個(gè)擴(kuò)展的核心就是我們今天的主角—— runtime逞盆。

何為runtime

runtime 其實(shí)是一個(gè)系統(tǒng)動態(tài)共享庫, 具有一個(gè)公共接口, 該公共接口由頭文件中的一組函數(shù)和數(shù)據(jù)結(jié)構(gòu)組成 (純C語言API). 由于所有的OC代碼終將轉(zhuǎn)換成C代碼, 使得 runtime 的API調(diào)用非常頻繁, 所以新版runtime里面對應(yīng)的實(shí)現(xiàn)基本上都是用C++和匯編語言混合來寫的, 以便提高系統(tǒng)效率.

runtime原理

這一部分的討論, 將圍繞檀蹋、實(shí)例令野、屬性垄懂、方法以及類別等在runtime中的表現(xiàn)形式來展開.

1. id --> objc_object

id是一個(gè)指向類實(shí)例的指針, 它在runtime中的定義如下:

typedef struct objc_object *id;

而objc_object在objc-private.h中定義如下:

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    ... 內(nèi)容太多故省略
}

objc_object結(jié)構(gòu)體包含一個(gè)isa指針, 根據(jù)isa就可以順藤摸瓜找到對象所屬的類. 而isa的類型isa_t使用union實(shí)現(xiàn), 可能表示多種形態(tài), 既可以當(dāng)成是指針, 也可以存儲標(biāo)志位. 這是蘋果提出的Tagged Pointer類型對象的概念, 目的是為了減少內(nèi)存資源的浪費(fèi). 畢竟用 64 bit 存儲一個(gè)內(nèi)存地址顯然是種浪費(fèi).

Tagged Pointer類型的對象采用一個(gè)跟機(jī)器字長一樣長度的整數(shù)來表示一個(gè)OC對象,而為了跟普通OC對象區(qū)分開來匣吊,每個(gè)Tagged Pointer類型對象的最高位為1而普通的OC對象的最高位為0.

小結(jié): OC中的對象終將轉(zhuǎn)換成C中的結(jié)構(gòu)體objc_object.

2. Class --> objc_class

我們在Xcode中輸入基類NSObject, 然后 ?+單擊 這個(gè)NSObject, 查看它的定義:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

其中省略了#pragma clang部分, 有興趣可查閱這篇博文#pragma

可以看到, NSObject有且僅有一個(gè)Class類型的isa屬性, 也就是說, 一個(gè)類對象唯一保存的信息就是它的 Class 的地址. 由于OC中幾乎所有的類(NSProxy等除外)都直接或間接地繼承于NSObject類, 可以說, OC中的類都有一個(gè)isa屬性. 那么這個(gè)isa又是什么呢?

我們繼續(xù) ?+單擊 Class, 可以看到他在runtime中的定義:

typedef struct objc_class *Class;

此時(shí)我們發(fā)現(xiàn), Class在runtime中是一個(gè)指向objc_class結(jié)構(gòu)體的指針.

isa, 意思是is a, 這是一個(gè)...

繼續(xù)查看objc_class結(jié)構(gòu)體, 我們看到它在Xcode的runtime.h里定義如下:

/**
 * objc1.0
 */
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

注意后面這個(gè)OBJC2_UNAVAILABLE
其實(shí)上面這個(gè)是兼容objc1.0版本的定義, 而目前我們使用的是objc2.0版本, 2.0中沒有暴露出bjc_class所定義的詳細(xì)內(nèi)容.

你可以在https://opensource.apple.com/source/objc4/objc4-723/中下載和查看開源的最新版本的Runtime庫源代碼舅逸。Runtime庫的源代碼是用匯編和C++混合實(shí)現(xiàn)的桌肴,你可以在頭文件objc-runtime-new.h中看到關(guān)于struct objc_class結(jié)構(gòu)的詳細(xì)定義。

/**
 * objc2.0
 */
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    ... 內(nèi)容太多故省略
}

objc_class結(jié)構(gòu)體中太多字段了所以這里省略掉. 其內(nèi)容主要有:包括類的名字堡赔、所繼承的基類识脆、類中定義的方法列表描述、屬性列表描述善已、實(shí)現(xiàn)的協(xié)議描述灼捂、定義的成員變量描述等等信息。如圖:

objc_class.png

objc_class繼承于objc_object, 也就是說一個(gè)OC類本身同時(shí)也是一個(gè)對象. 既然說類也是對象, 那么類的類型是什么呢换团?這里就引出了另外一個(gè)概念 —— Meta Class (元類).

小結(jié): OC中的類終將轉(zhuǎn)換成C中的結(jié)構(gòu)體objc_class.

3. Meta Class 元類

為了處理類和對象的關(guān)系, runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西.

其實(shí)觀察objc_classobjc_object的定義, 會發(fā)現(xiàn)兩者本質(zhì)相同(都包含isa指針), 只是objc_class多了一些額外的字段. 這些字段包括了創(chuàng)建一個(gè)類實(shí)例所需的信息, 以及這些實(shí)例的方法等. 那么類的信息和類方法儲存在哪呢? 答案是在元類里.

我們來看下這張著名的圖:

Meta Class.jpg

小結(jié):

  • 實(shí)例的isa指針指向類, 類的isa指針指向元類.
  • 類所對應(yīng)的objc_class里儲存了實(shí)例的方法, 元類所對應(yīng)的objc_class里儲存了類方法.
  • 元類的isa指針指向自己, 形成閉環(huán).
4. Ivar 成員變量 和 objc_property_t 屬性

Ivar

Ivar: instance variable

Ivar 代表類實(shí)例的變量或?qū)傩?帶下劃線"_"), 其在runtime中定義如下:

typedef struct ivar_t *Ivar;

ivar_t最終嵌套在objc_class里, 在objc-rentime-new.h中的結(jié)構(gòu)體層級關(guān)系如下:

ivar_t -> ivar_list_t -> class_ro_t -> class_rw_t -> class_data_bits_t -> objc_class

我們可以遍歷一個(gè)類的成員變量和屬性(加"_"):

// 打印成員變量列表
- (void)logIvarList {
    
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    for (int i=0; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"成員變量%d: %@", i, [NSString stringWithUTF8String:ivarName]);
    }
    
    free(ivarList);
}

objc_property_t
@property 標(biāo)記了類中的屬性, 它是一個(gè)指向objc_property 結(jié)構(gòu)體的指針:

typedef struct property_t *objc_property_t;

遍歷屬性:

// 打印屬性列表
- (void)logPropertyList {
    
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    
    for (unsigned int i=0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"屬性%d: %@", i, [NSString stringWithUTF8String:propertyName]);
    }
    
    free(propertyList);
}
5. Method / SEL / IMP 方法

Method
Method在runtime中定義如下:

typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
} 

SEL: selector
IMP: implementation

Method中存儲了這三樣?xùn)|西:

  • SEL類型的方法名.
  • char指針的方法類型, 指向存儲方法的參數(shù)類型和返回值類型.
  • IMP類型的方法實(shí)現(xiàn)地址.

SEL

Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

SEL是一個(gè)方法選擇器, 在runtime中其實(shí)是一個(gè)C字符串, 用來表示方法名稱.

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);          
// 打印: viewDidLoad

SEL是由編譯器在裝載類的時(shí)候自動生成的, 因此我們不能強(qiáng)制將一個(gè)C字符串轉(zhuǎn)化為SEL. 我們可以使用OC編譯器命令@selector()或者runtime系統(tǒng)的sel_registerName函數(shù)來獲得一個(gè) SEL 類型的方法選擇器. 例如:

SEL sel1 = @selector(viewWillAppear:);
SEL sel2 = sel_registerName("init");

不同類中相同名字的方法所對應(yīng)的方法選擇器是相同的, 即使方法名字相同而變量類型不同也會導(dǎo)致它們具有相同的方法選擇器. 比如, 同一個(gè)類中, 相同方法名不同參數(shù)類型也是會報(bào)錯(cuò)的:

// 同一個(gè)類中, 相同方法名不同參數(shù)類型, 報(bào)錯(cuò)
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

IMP
IMP在runtime中的定義如下:

/// A pointer to the function of a method implementation.
typedef void (IMP)(void / id, SEL, ... */ ); 

IMP是一個(gè)函數(shù)指針, 指向了方法實(shí)現(xiàn)的首地址.

這里要注意悉稠, IMP 指向的函數(shù)的前兩個(gè)參數(shù)是默認(rèn)參數(shù), id 和 SEL 艘包。這里的 SEL 好理解的猛,就是函數(shù)名。而 id 想虎,對于實(shí)例方法來說卦尊, self 保存了當(dāng)前對象的地址;對于類方法來說舌厨, self 保存了當(dāng)前對應(yīng)類對象的地址岂却。后面的省略號即是參數(shù)列表。

小結(jié):
Method / SEL / IMP 這三個(gè)概念之間關(guān)系: 在運(yùn)行時(shí), 類(Class)維護(hù)了一個(gè)消息分發(fā)列表來解決消息的正確發(fā)送. 每一個(gè)消息列表的入口是一個(gè)方法(Method), 這個(gè)方法映射了一對鍵值對, 其中鍵值是這個(gè)方法的名字 selector (SEL), 值是指向這個(gè)方法實(shí)現(xiàn)的函數(shù)指針 implementation (IMP).

6. Category

Category 為現(xiàn)有的類提供了拓展性, 它是 objc_category 結(jié)構(gòu)體的指針.

typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}       

雖然在OC2中不是這樣定義, 但是都有這些內(nèi)容, 為簡潔起見, 姑且這樣分析討論吧.

其中包含對象方法列表、類方法列表躏哩、協(xié)議列表等. 從這里我們也可以看出, Category 支持添加對象方法署浩、類方法、協(xié)議, 但不能保存成員變量.

注意:在 Category 中是可以添加屬性的扫尺,但不會生成對應(yīng)的成員變量筋栋、 getter 和 setter 。因此正驻,調(diào)用 Category 中聲明的屬性時(shí)會報(bào)錯(cuò)弊攘。

關(guān)聯(lián)對象
我們可以通過關(guān)聯(lián)對象的方式來添加可用的屬性:

- (void)setXxx:(NSString *)xxx {
    
    objc_setAssociatedObject(self, &xxx, xxx, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)xxx {
    
    return objc_getAssociatedObject(self, &xxx);
}

消息發(fā)送流程

當(dāng)我們在OC中執(zhí)行一個(gè)方法:

[receiver message]

編譯器會編譯成運(yùn)行時(shí)的C代碼:

objc_msgSend(receiver, selector)

如果消息含有參數(shù), 則為:

objc_msgSend(receiver, selector, arg1, arg2, ...)

其實(shí)有四個(gè)消息發(fā)送方法: objc_msgSend, objc_msgSend_stret, objc_msgSendSuperobjc_msgSendSuper_stret。如果消息是傳遞給超類拨拓,那么會調(diào)用名字帶有”Super”的函數(shù)肴颊;如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時(shí),那么會調(diào)用名字帶有”stret”的函數(shù)渣磷。“stret”可分為“st”+“ret”兩部分授瘦,分別代表“struct”和“return”醋界。

對于[receiver message], 在編譯階段確定了要向接收者 receiver 發(fā)送 message 這條消息,而 receive 將要如何響應(yīng)這條消息, 那就要看運(yùn)行時(shí)發(fā)生的情況來決定了.

以下是objc_msgSend消息發(fā)送流程:

  1. 檢測這個(gè)selector是不是要被忽略的提完。比如 Mac OS X 開發(fā)形纺,有了垃圾回收就不理會 retain, release 這些函數(shù)了。
  2. 檢測這個(gè)target對象是不是nil對象徒欣。(nil對象執(zhí)行任何一個(gè)方法都不會Crash逐样,因?yàn)闀缓雎缘簦?/li>
  3. 首先會根據(jù)target(objc_object)對象的isa指針獲取它所對應(yīng)的類(objc_class)
  4. 查看緩存cache中是否存在方法打肝。 如果有脂新,則找到objc_method中的IMP類型(函數(shù)指針)的成員method_imp去找到實(shí)現(xiàn)內(nèi)容,并執(zhí)行; 如果沒有粗梭,那么到該類的方法表(methodLists)查找該方法争便,依次從后往前查找。
  5. 如果沒有在類(class)找到断医,再到父類(super_class)查找滞乙,直至根類。
  6. 一旦找到與選擇子(selector)名稱相符的方法鉴嗤,就跳至其實(shí)現(xiàn)代碼斩启。
  7. 如果沒有找到,就會執(zhí)行消息轉(zhuǎn)發(fā)(message forwarding)的第一步動態(tài)解析醉锅。

消息轉(zhuǎn)發(fā)流程

先來看看這張圖:

消息轉(zhuǎn)發(fā).png

向不處理該消息的對象發(fā)送消息是錯(cuò)誤的. 但是, 在宣布錯(cuò)誤之前, 運(yùn)行時(shí)系統(tǒng)會給接收對象第二次處理消息的機(jī)會. 這個(gè)機(jī)會分三步走:

  1. 動態(tài)方法解析
  2. 接收者重定向
  3. 消息重定向

1. 動態(tài)方法解析

我們可以通過分別重載+resolveInstanceMethod:+resolveClassMethod:方法, 分別添加實(shí)例方法實(shí)現(xiàn)和類方法實(shí)現(xiàn). 然后返回YES, 運(yùn)行時(shí)系統(tǒng)就會重新啟動一次消息發(fā)送的過程.

??

// Person.h

@interface Person : NSObject

+ (void)eat;
- (void)work;

@end
// Person.m

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if(sel == @selector(eat)){
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(kk_eat)), "v@");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

// 添加實(shí)例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if(sel == @selector(work)){
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(kk_work)), "v@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 新的類方法
+ (void)kk_eat {
    NSLog(@"%s", __func__);
}

// 新的實(shí)例方法
- (void)kk_work {
    NSLog(@"%s", __func__);
}

@end
    // 類方法
    [Person eat];
    // 對象方法
    Person *person = [Person new];
    [person work];
// --------------------------------------------------------------
打印:
+[Person kk_eat]
-[Person kk_work]

注: 添加的方法必須實(shí)現(xiàn), 否則報(bào)錯(cuò)! 比如上述代碼中的kk_eatkk_work必須實(shí)現(xiàn).

關(guān)于class_addMethod的最后一個(gè)參數(shù)types
我們使用C函數(shù)來說明會比較好理解, 比如:

void eat(id self, SEL _cmd, NSString *str)
{
    NSLog(@"%@", str);
}

那么types參數(shù)為"v @ : @“, 按順序分別表示:

  • v: 返回值類型void, 若是i則表示int
  • @: 參數(shù)id(self)
  • :: SEL(_cmd)
  • @: id(str)

更多類型詳見Type Encodings

2. 接收者重定向

如果動態(tài)方法解析部分中, +resolveInstanceMethod:+resolveClassMethod:都返回了NO, 則會分別調(diào)用重定向類方法+forwardingTargetForSelector:和重定向?qū)嵗椒?code>-forwardingTargetForSelector:. 在這兩個(gè)方法中, 我們可以指定新的消息接收者, 但要注意的是新的接受者必須實(shí)現(xiàn)了該消息.

?? ??

新建一個(gè)類Alien.

// Alien.h

@interface Alien : NSObject

+ (void)eat;
- (void)work;

@end
// Alien.m

#import "Alien.h"

@implementation Alien

+ (void)eat {
    NSLog(@"%s", __func__);
}

- (void)work {
    NSLog(@"%s", __func__);
}

@end

在原來的Person類的+resolveInstanceMethod:+resolveClassMethod:方法里返回NO. 然后重寫重定向類方法+forwardingTargetForSelector:和重定向?qū)嵗椒?code>-forwardingTargetForSelector:.

#import "Person.h"
#import <objc/runtime.h>
#import "Alien.h"

@implementation Person

// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if(sel == @selector(eat)){
        return NO;
//        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(kk_eat)), "v@:");
//        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

// 添加實(shí)例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if(sel == @selector(work)){
        return NO;
//        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(kk_work)), "v@:");
//        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 重定向類方法:返回一個(gè)類
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(eat)) {
        return [Alien class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 重定向?qū)嵗椒ǎ悍祷匾粋€(gè)實(shí)例
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(work)) {
        Alien *alien = [Alien new];
        return alien;
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 新的類方法
+ (void)kk_eat {
    NSLog(@"%s", __func__);
}

// 新的實(shí)例方法
- (void)kk_work {
    NSLog(@"%s", __func__);
}

打印的不是Person的方法, 而是Alien的方法:

log:
+[Alien eat]
-[Alien work]

3. 消息重定向

如果
對于類方法, +resolveClassMethod:返回NO, +forwardingTargetForSelector:返回nil;
對于實(shí)例方法, +resolveInstanceMethod:返回NO, -forwardingTargetForSelector:返回nil.
那么
進(jìn)入第三步也是最后一步 —— 消息重定向.

消息重定向又分為兩個(gè)小步驟:

  1. runtime系統(tǒng)會向?qū)ο蟀l(fā)送-methodSignatureForSelector消息, 并取到返回的方法簽名用于生成NSInvocation對象;
  2. 將生成的NSInvocation對象作為參數(shù)調(diào)用-forwardInvocation:

?? ?? ??

// ViewController.m

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ViewController沒有work方法, 將會走轉(zhuǎn)發(fā)
    [self performSelector:@selector(work)];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 返回YES兔簇,進(jìn)入下一步轉(zhuǎn)發(fā)
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 返回nil,進(jìn)入下一步轉(zhuǎn)發(fā)
}


// 返回一個(gè)NSInvocation對象
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        // 生成方法簽名
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return methodSignature;
}


// 消息轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    SEL sel = anInvocation.selector;

//    // 類方法
//    if ([[Alien class] respondsToSelector:sel]) {
//        [anInvocation invokeWithTarget:[Alien class]];
//    }else{
//        // 若無法響應(yīng), 則報(bào)錯(cuò): 找不到響應(yīng)方法
//        [self doesNotRecognizeSelector:sel];
//    }
    
    // 實(shí)例方法
    Alien *alien = [Alien new];
    if ([alien respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:alien];
    }else{
        // 若無法響應(yīng), 則報(bào)錯(cuò): 找不到響應(yīng)方法
        [self doesNotRecognizeSelector:sel];
    }
}

log:

-[Alien work]

Method Swizzling

消息轉(zhuǎn)發(fā)雖然功能強(qiáng)大,但需要我們了解并且能更改對應(yīng)類的源代碼男韧,因?yàn)槲覀冃枰獙?shí)現(xiàn)自己的轉(zhuǎn)發(fā)邏輯朴摊。當(dāng)我們無法觸碰到某個(gè)類的源代碼,卻想更改這個(gè)類某個(gè)方法的實(shí)現(xiàn)時(shí)此虑,該怎么辦呢甚纲?可能繼承類并重寫方法是一種想法,但是有時(shí)無法達(dá)到目的朦前。這里介紹的是 Method Swizzling 介杆,它通過重新映射方法對應(yīng)的實(shí)現(xiàn)來達(dá)到“偷天換日”的目的。跟消息轉(zhuǎn)發(fā)相比韭寸,Method Swizzling 的做法更為隱蔽春哨,甚至有些冒險(xiǎn),也增大了debug的難度恩伺。

這里摘抄一個(gè) 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

參考文檔

Objective-C Runtime
深入解構(gòu)objc_msgSend函數(shù)的實(shí)現(xiàn)
Runtime-iOS運(yùn)行時(shí)基礎(chǔ)篇
iOS Runtime詳解
runtime開源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赴背,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子晶渠,更是在濱河造成了極大的恐慌凰荚,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褒脯,死亡現(xiàn)場離奇詭異便瑟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)番川,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門到涂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颁督,你說我怎么就攤上這事践啄。” “怎么了适篙?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵往核,是天一觀的道長。 經(jīng)常有香客問我嚷节,道長聂儒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任硫痰,我火速辦了婚禮衩婚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘效斑。我一直安慰自己非春,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奇昙,像睡著了一般护侮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上储耐,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天羊初,我揣著相機(jī)與錄音,去河邊找鬼什湘。 笑死长赞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闽撤。 我是一名探鬼主播得哆,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哟旗!你這毒婦竟也來了贩据?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤闸餐,失蹤者是張志新(化名)和其女友劉穎乐设,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绎巨,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年蠕啄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了场勤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡歼跟,死狀恐怖和媳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哈街,我是刑警寧澤留瞳,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站骚秦,受9級特大地震影響她倘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜作箍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一硬梁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胞得,春花似錦荧止、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽危号。三九已至,卻和暖如春素邪,著一層夾襖步出監(jiān)牢的瞬間外莲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工娘香, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苍狰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓烘绽,卻偏偏與公主長得像淋昭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子安接,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345