你要知道的runtime都在这里

转载请注明出处 http://blog.csdn.net/u014205968/article/details/67639289

本文主要解说runtime相关知识,从原理到实践,由于包括内容过多分为下面五篇文章详细解说,可自行选择须要了解的方向:

本文是系列文章的第二篇文章从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消息转发机制的更多相关文章

  1. iOS runtime探究(三): 从runtime開始理解OC的属性property

    你要知道的runtime都在这里 转载请注明出处 http://blog.csdn.net/u014205968/article/details/67639303 本文主要解说runtime相关知识, ...

  2. iOS Runtime的消息转发机制

    前面我们已经讲解Runtime的基本概念和基本使用,如果大家对Runtime机制不是很了解,可以先看一下以前的博客,会对理解这篇博客有所帮助!!! Runtime基本概念:https://www.cn ...

  3. 理解Objective-C Runtime(三)消息转发机制

    消息转发机制概述 上一篇博客消息传递机制中讲解了Objective-C中对象的「消息传递机制」.本文需要讲解另外一个重要问题:当对象受到无法处理的消息之后会发生什么情况? 显然,若想令类能理解某条消息 ...

  4. iOS的消息转发机制详解

    iOS开发过程中,有一类的错误会经常遇到,就是找不到所调用的方法,当然这类问题比较好解决,给当前对象或其父类对象添加该方法即可,使得编译器在编译时能正确找到该方法:或者,还有另外的方法,由于Objec ...

  5. iOS 消息转发机制

    这篇博客的前置知识点是 OC 的消息传递机制,如果你对此还不了解,请先学习之,再来看这篇.这篇博客我尝试用口语的方式像讲述 PPT 一样给大家讲述这个知识点. 我们来思考一个问题,如果对象在收到无法解 ...

  6. iOS消息转发机制

    iOS消息转发机制 “消息派发系统”(message-dispatch system) 若想令类能够理解某条消息,我们必须实现出对应的方法才行.但是,在编译器向类发送其无法解读的消息时并不会报错,因为 ...

  7. ios开发runtime学习二:runtime交换方法

    #import "ViewController.h" /* Runtime(交换方法):主要想修改系统的方法实现 需求: 比如说有一个项目,已经开发了2年,忽然项目负责人添加一个功 ...

  8. runtime消息转发机制

    Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制.而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库.它是 Objective- ...

  9. OC:浅析Runtime中消息转发机制

    一.介绍 OC是一门动态性语言,其实现的本质是利用runtime机制.在runtime中,对象调用方法,其实就是给对象发送一个消息,也即objc_msgSend().在这个消息发送的过程中,系统会进行 ...

随机推荐

  1. Oracle EBS WMS功能介绍(二)

    Oracle EBS WMS功能介绍(二) (版权声明,本人原创或者翻译的文章如需转载,如转载用于个人学习,请注明出处.否则请与本人联系,违者必究) 出货物流逻辑主要包括 1.      打包.能够进 ...

  2. 【LeetCode】136. Single Number (4 solutions)

    Single Number Given an array of integers, every element appears twice except for one. Find that sing ...

  3. Android JUnit 入门指南

    自动化单元测试可以做许多的事,并帮你节省时间.它也可以被用作快速检验新建工程或进行冒烟测试.始终,单元测试是作为一种有效的.系统的检验应用程序各功能执行的方式.Android SDK支持JUnit的自 ...

  4. WCF 的 WebGet 方式

    .NET 3.5以后,WCF中提供了WebGet的方式,允许通过url的形式进行Web 服务的访问.在以前的代码中,写过多次类似的例子,但总是忘记如何配置,现在将设置步骤记录如下: endpoint通 ...

  5. 标头“Vary:Accept-Encoding”指定方法[转]

    现在的新浏览器都支持压缩了,因此如果网站启用了GZip,可以无需再指定“Vary: Accept-Encoding”标头,不过指定“Vary: Accept-Encoding”标头会有更高的保险,而它 ...

  6. setContentView(R.layout.activity_main)无法正常引用

    今天在写Android代码的过程中,编译器一直报错,错误出在这一行代码: setContentView(R.layout.activity_main) 提示信息是: activity_main can ...

  7. 三十道linux内核面试题

      1. Linux中主要有哪几种内核锁? Linux的同步机制从2.0到2.6以来不断发展完善.从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁.这些同步机制的发展伴随Linux从单处理器 ...

  8. STM32F103 AFIO时钟疑问

    在stm32F103系列中:AFIO是重映射辅助时钟,如果仅仅是使用第二功能(如uart,spi,),不需要打开,使用第二功能打开GPIO和第二功能时钟.我反复测试是这样的 AFIO时钟由RCC_AP ...

  9. 问题解决:在此页上的ActiveX控件

    打开什么美图秀秀就会弹出windows安全警告?网易闪电邮每打开一封邮件就会出现安全警告?这个对话框无论你点是否,都会再次出现!! 网上的方法教你改ie设置 教你改UAC 通通不好用!!!重装系统也不 ...

  10. 今天遇到的一个bug,折腾了一早上,不过解决了,还是很高兴

    1.总结出错的问题 当我在用flask做项目的时候,需要创建表,创建表的时候,我用的是Flask-Migrate组件,直接用python manage.py init ,python manage.p ...