1. 程式人生 > >iOS runtime探究(二): 從runtime開始深入理解OC消息轉發機制

iOS runtime探究(二): 從runtime開始深入理解OC消息轉發機制

phoenix face exp nslog void string ams ber 解釋

你要知道的runtime都在這裏

轉載請註明出處 http://blog.csdn.net/u014205968/article/details/67639289

本文主要解說runtime相關知識,從原理到實踐。由於包括內容過多分為下面五篇文章詳細解說。可自行選擇須要了解的方向:

  • 從runtime開始: 理解面向對象的類到面向過程的結構體
  • 從runtime開始: 深入理解OC消息轉發機制
  • 從runtime開始: 理解OC的屬性property
  • 從runtime開始: 實踐Category加入屬性與黑魔法method swizzling
  • 從runtime開始: 深入weak實現機理

本文是系列文章的第二篇文章從runtime開始: 深入理解OC消息轉發機制,主要從runtime出發解說OC的消息傳遞和消息轉發機制。

你不知道的msg_send

我們知道在OC中的實例對象調用一個方法稱作消息傳遞,比方有例如以下代碼:

NSMutableString *str = [[NSMutableString alloc] initWithString: @"Jiaming Chen"];
[str appendString:@" is a good guy."];

上述代碼中的第二句str稱為消息的接受者。appendString:

稱作選擇子也就是我們經常使用的selectorselector參數共同構成了消息,所以第二句話能夠理解為將消息:"添加一個字符串: is a good guy"發送給消息的接受者str


OC中裏的消息傳遞採用動態綁定機制來決定詳細調用哪個方法,OC的實例方法在轉寫為C語言後實際就是一個函數,可是OC並非在編譯期決定調用哪個函數,而是在執行期決定,由於編譯期根本不能確定終於會調用哪個函數,這是由於執行期能夠改動方法的實現,在後文會有解說。舉個栗子。有例如以下代碼:

id num = @123;
//輸出123
NSLog(@"%@", num);
//程序崩潰,報錯[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];

上述代碼在編譯期沒有不論什麽問題,由於id類型能夠指向不論什麽類型的實例對象。NSString有一個方法appendString:,在編譯期不確定這個num究竟詳細指代什麽類型的實例對象,而且在執行期還能夠給NSNumber類型加入新的方法。因此編譯期發現有appendString:的函數聲明就不會報錯,但在執行時找不到在NSNumber類中找不到appendString:方法,就會報錯。這也就是消息傳遞的強大之處和弊端,編譯期無法檢查到沒有定義的方法,執行期能夠加入新的方法。

講了這麽多OC究竟是怎麽將實例方法轉換為C語言的函數,又是怎樣調用這些函數的呢?這些都依靠強大的runtime

在深入代碼之前介紹一個clang編譯器的命令:

clang -rewrite-objc main.m
該命令能夠將.m的OC文件轉寫為.cpp文件

有例如以下代碼:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

- (void)showMyself;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

- (void)showMyself {
    NSLog(@"My name is %@ I am %ld years old.", self.name, self.age);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
         //為了方便查看轉寫後的C語言代碼。將alloc和init分兩步完畢
        Person *p = [Person alloc];
        p = [p init];
        p.name = @"Jiaming Chen";
        [p showMyself];
    }
    return 0;
}

通過上述clang命令能夠轉寫代碼。然後找到例如以下定義:

static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }

// @synthesize age = _age;
static NSUInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, NSUInteger age) { (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }

static void _I_Person_showMyself(Person * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")), ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
}

// @end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
        p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));

    }
    return 0;
}

關於屬性property生成的gettersetter和實例變量相關代碼在還有一篇博客iOS @property探究(二): 深入理解中有詳細介紹,本文不再贅述,本文僅針對自己定義的方法來解說。

能夠發現轉寫後的C語言代碼將實例方法轉寫為了一個靜態函數。接下來一行一行的分析上述代碼,第一行代碼能夠簡要表示為例如以下代碼:

Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

這一行代碼做了三件事情。第一獲取Person類,第二註冊alloc方法,第三發送消息,將消息alloc發送給類對象,能夠簡單的將註冊方法理解為。通過方法名獲取到轉寫後C語言函數的函數指針。
第二行代碼就能夠簡寫為例如以下代碼:

p = objc_msgSend(p, sel_registerName("init"));

這一行代碼與上一行相似,註冊了init方法,然後通過objc_msgSend函數將消息init發送給消息的接受者p


第三行是一個對setter的調用,相同的也能夠簡寫為例如以下代碼:

//這一行是用來查找參數的地址。取名為name
(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1)
objc_msgSend(p, sel_registerName("setName:"), name);

這一行代碼相同是先註冊方法setName:然後通過objc_msgSend函數將消息setName:發送給消息的接收者。僅僅是多了一個參數的傳遞。
同理,最後一行代碼也能夠簡寫為例如以下:

objc_msgSend(p, sel_registerName("showMyself"));

解釋與上述相同,不再贅述。

到這裏。我們應該就能夠看出OC的runtime通過objc_msgSend函數將一個面向對象的消息傳遞轉為了面向過程的函數調用。
objc_msgSend函數依據消息的接受者和selector選擇適當的方法來調用。那它又是怎樣選擇的呢?這就涉及到前一篇博客解說的內容iOS runtime探究(一): 從runtime開始: 理解面向對象的類到面向過程的結構體。這一篇博客中詳細解說了OC的runtime是怎樣將面向對象的類映射為面向過程的結構體的。再來回想一下幾個基本的結構體:

文件objc/runtime.h中有例如以下定義:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

    Class super_class                                        
    const char *name                                         
    long version                                             
    long info                                                
    long instance_size                                       
    struct objc_ivar_list *ivars                             
    struct objc_method_list **methodLists                    
    struct objc_cache *cache                                 
    struct objc_protocol_list *protocols                     
}
/* Use `Class` instead of `struct objc_class *` */

文件objc/objc.h文件裏有例如以下定義
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

註意結構體struct objc_class中包括一個成員變量struct objc_method_list **methodLists。通過名稱我們分析出這個成員變量保存了實例方法列表,繼續查找結構體struct objc_method_list的定義例如以下:

static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        5,
        {{(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
        {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
        {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
        {(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
        {(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};

struct _objc_method {
        struct objc_selector * _cmd;
        const char *method_type;
        void  *_imp;
};

我們發現struct objc_method_list中還包括了一個未知的結構體struct _objc_method同一時候也找到它的定義,為了方便查看將兩者寫在一起。
結構體struct objc_method_list裏面包括下面幾個成員變量:結構體struct _objc_method的大小、方法個數以及最重要的方法列表,方法列表存儲的是方法描寫敘述結構體struct _objc_method,該結構體裏保存了選擇子、方法類型以及方法的詳細實現。能夠看出方法的詳細實現就是一個函數指針,也就是我們自己定義的實例方法。選擇子也就是selector能夠理解為是一個字符串類型的名稱,用於查找相應的函數實現(由於蘋果沒有開源selector的相關代碼,可是能夠查到GNU OC中關於selector的定義,也是一個結構體可是結構體裏存儲的就是一個字符串類型的名稱)。

這樣就能解釋objc_msgSend的工作原理的,為了匹配消息的接收者和選擇子。須要在消息的接收者所在的類中去搜索這個struct objc_method_list方法列表,假設能找到就能夠直接跳轉到相關的詳細實現中去調用。假設找不到,那就會通過super_class指針沿著繼承樹向上去搜索,假設找到就跳轉,假設到了繼承樹的根部(通常為NSObject)還沒有找到,那就會調用NSObjec的一個方法doesNotRecognizeSelector:。這種方法就會報unrecognized selector錯誤(事實上在調用這種方法之前還會進行消息轉發,還有三次機會來處理,消息轉發在後文會有介紹)。

這樣一看。要發送消息真的好復雜,須要經過這麽多步驟,難道不會影響性能嗎?當然了。這樣一次次搜索和靜態綁定那樣直接跳轉到函數指針指向的位置去執行來比肯定是耗時非常多的。因此,類對象也就是結構體struct objc_class中有一個成員變量struct objc_cache,這個緩存裏緩存的正是搜索方法的匹配結果。這樣在第二次及以後再訪問時就能夠採用映射的方式找到相關實現的詳細位置。

到這裏我們就已經弄清晰了整個發送消息的過程,可是當對象無法接收相關消息時又會發生什麽?以及前文說的三次機會又是什麽?下文將會介紹消息轉發。

消息轉發: unrecognized selector的最後三次機會

還是那個栗子:

id num = @123;
//輸出123
NSLog(@"%@", num);
//程序崩潰,報錯[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];

前文介紹了進行一次發送消息會在相關的類對象中搜索方法列表。假設找不到則會沿著繼承樹向上一直搜索知道繼承樹根部(通常為NSObject),假設還是找不到而且消息轉發都失敗了就回執行doesNotRecognizeSelector:方法報unrecognized selector錯。那麽消息轉發究竟是什麽呢?接下來將會逐一介紹最後的三次機會。

第一次機會: 所屬類動態方法解析

首先。假設沿繼承樹沒有搜索到相關方法則會向接收者所屬的類進行一次請求,看能否夠動態的加入一個方法,註意這是一個類方法。由於是向接收者所屬的類進行請求。

+(BOOL)resolveInstanceMethod:(SEL)name

舉個栗子吧:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;
//假設須要傳參直接在參數列表後面加入就好了
void dynamicAdditionMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamicAdditionMethodIMP");
}

+ (BOOL)resolveInstanceMethod:(SEL)name {
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
    if (name == @selector(appendString:)) {
        class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:name];
}

+ (BOOL)resolveClassMethod:(SEL)name {
    NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
    return [super resolveClassMethod:name];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id p = [[Person alloc] init];
        [p appendString:@""];
    }
    return 0;
}

先看一下最後的輸出結果吧:

2017-03-24 19:05:25.092404 OCTest[5142:1185077] resolveInstanceMethod: appendString:
2017-03-24 19:05:25.092810 OCTest[5142:1185077] dynamicAdditionMethodIMP

先看一下main函數,首先創建了一個Person的實例對象,一定要用id類型來聲明,否則會在編譯期就報錯。由於找不到相關函數的聲明。id類型由於能夠指向不論什麽類型的對象。因此編譯時能夠找到NSString類的相關方法聲明就不會報錯。
由於Person類沒有聲明和定義appendString:方法,所以執行時應該會報unrecognized selector錯誤。可是並沒有,由於我們重寫了類方法+ (BOOL)resolveInstanceMethod:(SEL)name,當找不到相關實例方法的時候就會調用該類方法去詢問能否夠動態加入,假設返回True就會再次執行相關方法。接下來看一下怎樣給一個類動態加入一個方法。那就是調用runtime庫中的class_addMethod方法,該方法的原型是

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

通過參數名能夠看出第一個參數是須要加入方法的類。第二個參數是一個selector,也就是實例方法的名字。第三個參數是一個IMP類型的變量也就是函數實現,須要傳入一個C函數。這個函數至少有兩個參數,一個是id self一個是SEL _cmd,第四個參數是函數類型。

詳細設置方法能夠看凝視。

第二次機會: 備援接收者

當對象所屬類不能動態加入方法後,runtime就會詢問當前的接受者是否有其它對象能夠處理這個未知的selector。相關方法聲明例如以下:

- (id)forwardingTargetForSelector:(SEL)aSelector;

該方法的參數就是那個未知的selector,這是一個實例方法。由於是詢問該實例對象是否有其它實例對象能夠接收這個未知的selector,假設沒有就返回nil。能夠自行實驗。

第三次機會: 消息重定向

當沒有備援接收者時。就僅僅剩下最後一次機會,那就是消息重定向。

這個時候runtime會將未知消息的全部細節都封裝為NSInvocation對象。然後調用下述方法:

- (void)forwardInvocation: (NSInvocation*)invocation;

調用這種方法假設不能處理就會調用父類的相關方法。一直到NSObject的這種方法,假設NSObject都無法處理就會調用doesNotRecognizeSelector:方法拋出異常。

整個消息轉發流程例如以下圖所看到的:
技術分享圖片

總結

本文通過對runtime的分析。詳解了整個發送消息和消息轉發的流程,對OC的runtime能有一個更清晰的掌握。

下一步

這兩篇文章分別介紹了runtime怎樣將面向對象的類映射到面向過程的結構體以及runtime的消息發送和消息轉發流程,下一篇文章將繼續介紹runtime對實例變量的處理。感興趣的讀者能夠繼續學習下一篇文章從runtime開始: 理解OC的屬性property

備註

由於作者水平有限,難免出現紕漏,如有問題還請指教。

iOS runtime探究(二): 從runtime開始深入理解OC消息轉發機制