您的位置:首页 > 移动开发 > IOS开发

iOS runtime探究(二): 从runtime開始深入理解OC消息转发机制

2018-02-19 15:06 691 查看

你要知道的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:
称作
选择子
也就是我们经常使用的
selector
selector
參数
共同构成了
消息
,所以第二句话能够理解为将消息:
"添加一个字符串: 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
生成的
getter
setter
和实例变量相关代码在还有一篇博客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

备注


由于作者水平有限,难免出现纰漏,如有问题还请指教。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: