你要知道的runtime都在這里
轉載請注明出處 http://www.itdecent.cn/p/eac6ed137e06
本文主要講解runtime相關知識,從原理到實踐,由于包含內(nèi)容過多分為以下五篇文章詳細講解,可自行選擇需要了解的方向:
- 從runtime開始: 理解面向對象的類到面向過程的結構體
- 從runtime開始: 深入理解OC消息轉發(fā)機制
- 從runtime開始: 理解OC的屬性property
- 從runtime開始: 實踐Category添加屬性與黑魔法method swizzling
- 從runtime開始: 深入weak實現(xiàn)機理
本文是系列文章的第二篇文章從runtime開始: 深入理解OC消息轉發(fā)機制,主要從runtime出發(fā)講解OC的消息傳遞和消息轉發(fā)機制。
你不知道的msg_send
我們知道在OC中的實例對象調用一個方法稱作消息傳遞,比如有如下代碼:
NSMutableString *str = [[NSMutableString alloc] initWithString: @"Jiaming Chen"];
[str appendString:@" is a good guy."];
上述代碼中的第二句str稱為消息的接受者,appendString:稱作選擇子也就是我們常用的selector,selector和參數(shù)共同構成了消息,所以第二句話可以理解為將消息:"增加一個字符串: is a good guy"發(fā)送給消息的接受者str。
OC中里的消息傳遞采用動態(tài)綁定機制來決定具體調用哪個方法,OC的實例方法在轉寫為C語言后實際就是一個函數(shù),但是OC并不是在編譯期決定調用哪個函數(shù),而是在運行期決定,因為編譯期根本不能確定最終會調用哪個函數(shù),這是由于運行期可以修改方法的實現(xiàn),在后文會有講解。舉個栗子,有如下代碼:
id num = @123;
//輸出123
NSLog(@"%@", num);
//程序崩潰,報錯[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];
上述代碼在編譯期沒有任何問題,因為id類型可以指向任何類型的實例對象,NSString有一個方法appendString:,在編譯期不確定這個num到底具體指代什么類型的實例對象,并且在運行期還可以給NSNumber類型添加新的方法,因此編譯期發(fā)現(xiàn)有appendString:的函數(shù)聲明就不會報錯,但在運行時找不到在NSNumber類中找不到appendString:方法,就會報錯。這也就是消息傳遞的強大之處和弊端,編譯期無法檢查到未定義的方法,運行期可以添加新的方法。
講了這么多OC究竟是怎么將實例方法轉換為C語言的函數(shù),又是如何調用這些函數(shù)的呢?這些都依靠強大的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生成的getter、setter和實例變量相關代碼在另一篇博客iOS @property探究(二): 深入理解中有詳細介紹,本文不再贅述,本文僅針對自定義的方法來講解。
可以發(fā)現(xiàn)轉寫后的C語言代碼將實例方法轉寫為了一個靜態(tài)函數(shù)。接下來一行一行的分析上述代碼,第一行代碼可以簡要表示為如下代碼:
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
這一行代碼做了三件事情,第一獲取Person類,第二注冊alloc方法,第三發(fā)送消息,將消息alloc發(fā)送給類對象,可以簡單的將注冊方法理解為,通過方法名獲取到轉寫后C語言函數(shù)的函數(shù)指針。
第二行代碼就可以簡寫為如下代碼:
p = objc_msgSend(p, sel_registerName("init"));
這一行代碼與上一行類似,注冊了init方法,然后通過objc_msgSend函數(shù)將消息init發(fā)送給消息的接受者p。
第三行是一個對setter的調用,同樣的也可以簡寫為如下代碼:
//這一行是用來查找參數(shù)的地址,取名為name
(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1)
objc_msgSend(p, sel_registerName("setName:"), name);
這一行代碼同樣是先注冊方法setName:然后通過objc_msgSend函數(shù)將消息setName:發(fā)送給消息的接收者,只是多了一個參數(shù)的傳遞。
同理,最后一行代碼也可以簡寫為如下:
objc_msgSend(p, sel_registerName("showMyself"));
解釋與上述相同,不再贅述。
到這里,我們應該就可以看出OC的runtime通過objc_msgSend函數(shù)將一個面向對象的消息傳遞轉為了面向過程的函數(shù)調用。
objc_msgSend函數(shù)根據(jù)消息的接受者和selector選擇適當?shù)姆椒▉碚{用,那它又是如何選擇的呢?這就涉及到前一篇博客講解的內(nèi)容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,通過名稱我們分析出這個成員變量保存了實例方法列表,繼續(xù)查找結構體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;
};
我們發(fā)現(xiàn)struct objc_method_list中還包含了一個未知的結構體struct _objc_method同時也找到它的定義,為了方便查看將兩者寫在一起。
結構體struct objc_method_list里面包含以下幾個成員變量:結構體struct _objc_method的大小、方法個數(shù)以及最重要的方法列表,方法列表存儲的是方法描述結構體struct _objc_method,該結構體里保存了選擇子、方法類型以及方法的具體實現(xiàn)??梢钥闯龇椒ǖ木唧w實現(xiàn)就是一個函數(shù)指針,也就是我們自定義的實例方法,選擇子也就是selector可以理解為是一個字符串類型的名稱,用于查找對應的函數(shù)實現(xiàn)(由于蘋果沒有開源selector的相關代碼,但是可以查到GNU OC中關于selector的定義,也是一個結構體但是結構體里存儲的就是一個字符串類型的名稱)。
這樣就能解釋objc_msgSend的工作原理的,為了匹配消息的接收者和選擇子,需要在消息的接收者所在的類中去搜索這個struct objc_method_list方法列表,如果能找到就可以直接跳轉到相關的具體實現(xiàn)中去調用,如果找不到,那就會通過super_class指針沿著繼承樹向上去搜索,如果找到就跳轉,如果到了繼承樹的根部(通常為NSObject)還沒有找到,那就會調用NSObjec的一個方法doesNotRecognizeSelector:,這個方法就會報unrecognized selector錯誤(其實在調用這個方法之前還會進行消息轉發(fā),還有三次機會來處理,消息轉發(fā)在后文會有介紹)。
這樣一看,要發(fā)送消息真的好復雜,需要經(jīng)過這么多步驟,難道不會影響性能嗎?當然了,這樣一次次搜索和靜態(tài)綁定那樣直接跳轉到函數(shù)指針指向的位置去執(zhí)行來比肯定是耗時很多的,因此,類對象也就是結構體struct objc_class中有一個成員變量struct objc_cache,這個緩存里緩存的正是搜索方法的匹配結果,這樣在第二次及以后再訪問時就可以采用映射的方式找到相關實現(xiàn)的具體位置。
到這里我們就已經(jīng)弄清楚了整個發(fā)送消息的過程,但是當對象無法接收相關消息時又會發(fā)生什么?以及前文說的三次機會又是什么?下文將會介紹消息轉發(fā)。
消息轉發(fā): unrecognized selector的最后三次機會
還是那個栗子:
id num = @123;
//輸出123
NSLog(@"%@", num);
//程序崩潰,報錯[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];
前文介紹了進行一次發(fā)送消息會在相關的類對象中搜索方法列表,如果找不到則會沿著繼承樹向上一直搜索知道繼承樹根部(通常為NSObject),如果還是找不到并且消息轉發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報unrecognized selector錯。那么消息轉發(fā)到底是什么呢?接下來將會逐一介紹最后的三次機會。
第一次機會: 所屬類動態(tài)方法解析
首先,如果沿繼承樹沒有搜索到相關方法則會向接收者所屬的類進行一次請求,看是否能夠動態(tài)的添加一個方法,注意這是一個類方法,因為是向接收者所屬的類進行請求。
+(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;
//如果需要傳參直接在參數(shù)列表后面添加就好了
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函數(shù),首先創(chuàng)建了一個Person的實例對象,一定要用id類型來聲明,否則會在編譯期就報錯,因為找不到相關函數(shù)的聲明,id類型由于可以指向任何類型的對象,因此編譯時能夠找到NSString類的相關方法聲明就不會報錯。
由于Person類沒有聲明和定義appendString:方法,所以運行時應該會報unrecognized selector錯誤,但是并沒有,因為我們重寫了類方法+ (BOOL)resolveInstanceMethod:(SEL)name,當找不到相關實例方法的時候就會調用該類方法去詢問是否可以動態(tài)添加,如果返回True就會再次執(zhí)行相關方法,接下來看一下如何給一個類動態(tài)添加一個方法,那就是調用runtime庫中的class_addMethod方法,該方法的原型是
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
通過參數(shù)名可以看出第一個參數(shù)是需要添加方法的類,第二個參數(shù)是一個selector,也就是實例方法的名字,第三個參數(shù)是一個IMP類型的變量也就是函數(shù)實現(xiàn),需要傳入一個C函數(shù),這個函數(shù)至少有兩個參數(shù),一個是id self一個是SEL _cmd,第四個參數(shù)是函數(shù)類型。具體設置方法可以看注釋。
第二次機會: 備援接收者
當對象所屬類不能動態(tài)添加方法后,runtime就會詢問當前的接受者是否有其他對象可以處理這個未知的selector,相關方法聲明如下:
- (id)forwardingTargetForSelector:(SEL)aSelector;
該方法的參數(shù)就是那個未知的selector,這是一個實例方法,因為是詢問該實例對象是否有其他實例對象可以接收這個未知的selector,如果沒有就返回nil,可以自行實驗。
第三次機會: 消息重定向
當沒有備援接收者時,就只剩下最后一次機會,那就是消息重定向。這個時候runtime會將未知消息的所有細節(jié)都封裝為NSInvocation對象,然后調用下述方法:
- (void)forwardInvocation: (NSInvocation*)invocation;
調用這個方法如果不能處理就會調用父類的相關方法,一直到NSObject的這個方法,如果NSObject都無法處理就會調用doesNotRecognizeSelector:方法拋出異常。
整個消息轉發(fā)流程如下圖所示:

總結
本文通過對runtime的分析,詳細解釋了整個發(fā)送消息和消息轉發(fā)的流程,對OC的runtime能有一個更清晰的掌握。
下一步
這兩篇文章分別介紹了runtime如何將面向對象的類映射到面向過程的結構體以及runtime的消息發(fā)送和消息轉發(fā)流程,下一篇文章將繼續(xù)介紹runtime對實例變量的處理。感興趣的讀者可以繼續(xù)學習下一篇文章從runtime開始: 理解OC的屬性property
備注
由于作者水平有限,難免出現(xiàn)紕漏,如有問題還請不吝賜教。