runtime第三部分方法和消息
接上一篇http://www.cnblogs.com/ddavidXu/p/5924049.html
转载来源http://www.jianshu.com/p/6b905584f536
http://southpeak.github.io/2014/10/30/objective-c-runtime-2/
方法和消息 OC中对象调用方法,实际是给对象发送消息
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
objc_selector结构体的详细定义没有在头文件中找到。方法的selector用于表示运行时方法的名字
OC在编译时,会依据每一个方法的名字参数序列,生成唯一的整形标识(int类型的地址)这个标识就SEL
SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);
-- ::57.820 XDWRuntimeDemo[:] sel : 0x106d14bd9
- 两个类之间,不管是不是父子关系,还是没有父子关系,只要方法名字相同,那个方法的SEL就是一样的;
- 每一个方法都对应着一个SEL,所以在OC的同一个类(及继承体系中)中,不能同时存在2个同名的方法,即使参数类型不同也不可以。
- 相同的方法只能对应一个SEL
- 当然,不同的类可以拥有相同的selector,因为不同的实例对象执行相同的selector时,会在各自的方法列表中根据selector去找自己对应的IMP
- 工程中所有的SEL组合成一个set集合,set特点就是唯一性,因此SEL是唯一的。当我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了。
- SEL实际上是根据方法名hash化了一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,速度非常非常的快,有一个问题就是,数量的增多会增大hash冲突而导致性能下降,将总量减少时最犀利的方法,为什么SEL仅仅是函数名。
- 本质上SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的key值,能唯一代表一个方法),它的存在是为了加快方法的查询速度
我们可以通过runtime添加新的selector,也可以通过runtime获取已存在的selector,
sel_registerName函数 Objective-C编译器提供的@selector() NSSelectorFromString()方法
IMP
imp实际上是一个函数指针,指向方法的实现首地址
id (*IMP)(id, SEL, ...)
- 这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
- 前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的 IMP,取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针 了
通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些.
Method
typedef struct objc_method *Method; struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码
struct objc_method_description { SEL name; char *types; };//方法描述
方法相关操作函数
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... ); //method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快。 // 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... ); // 获取方法名
SEL method_getName ( Method m ); //method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method)) // 返回方法的实现
IMP method_getImplementation ( Method m ); // 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m ); // 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );//类型字符串会被拷贝到dst中 // 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index ); // 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len ); // 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m ); // 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len ); // 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m ); // 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );//注意该函数返回值是方法之前的实现 // 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
方法选择器
// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel ); // 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str ); //在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器 // 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str ); // 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
方法调用流程
在OC中,消息直到运行时才绑定到方法实现上,编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend.这个函数将消息接受者的方法名作为其基础参数。
objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)
这个函数完成了动态绑定的所有事情:
1,首先它找到selector对应的方法实现IMP,因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖接收者的类来找到确切的实现。
2. 它调用方法实现,并将接收者对象及方法的所有参数传给它
3. 最后,它将实现返回的值作为它自己的返回值。
消息的关键在于结构体objc_class
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif } OBJC2_UNAVAILABLE;
在这个结构体中,我们需要注意
1.指向父类的指针 isa
2.一个类的方法分发表 ,methodlists
创建对象的过程
创建对象-->分配内存-->初始化成员变量(isa指针也会被初始化)
当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系
当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果 没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依 此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实 现。如果最后没有定位到selector,则会走消息转发流程,
消息发送给一个对象-->>objc_msgSend通过对象的isa指针获取到类的结构体-->在方法分发表里面查找方法的selector-->定位到selector,函数会就获取到了实现的入口点,
| 并传入相应的参数来执行方法的具体实现
|
没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector(知道NSObject类)
|
|
如果最后没有定位到selector,则会走消息转发流程,
为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址
隐藏参数
objc_msgSend有两个隐藏参数:
消息接收对象
方法的selector
这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。
虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。如下代码所示:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod(); if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
获取方法地址
runtime方法中的动态绑定让我们写代码更具有灵活性,比如我们可以把消息转发给我们想要的对象,或者随意交换两个方法的实现等。灵活性的提升也带来了一定的性能耗损,毕竟要查找方法的实现,不像调用函数那么简单,不过方法缓存一定程度上解决了这个问题
MethodForSelector:方法可以获取方法的指针。
IMP str = [self methodForSelector:@selector(setFilled:)];
消息转发
当一个对象调用一个方法时,即给这个对象发送一个消息,假如这个对象无法接受这个消息,即这个对象对应的类,以及对应类的父类中都没有找到这个方法,正常情况下,object无法响应message,编译器会报错,崩溃。
- (void)doesNotRecognizeSelector:(SEL)aSelector { //调用次方法崩溃
[super doesNotRecognizeSelector:aSelector];
}
但是如果使用perform的形式来调用方法,会等到运行时才能确定object是否能接收message消息,如果不能,则会崩溃。
(litttle tip)看一个对象是否能响应某个消息时进行检验
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
使用perform调用一个方法时,对象无法接受消息,就会启动消息转发机制,在程序崩溃前,我们有三次机会通过消息阻止程序崩溃。
消息转发机制基本上分为三个步骤:
动态方法解析
备用接收者
完整转发
对象接受未知消息--调用所属类的的类方法+resolveInstancheMethod:(实例方法)或者+resolveClassMethod:(类方法)--增加处理方法比如通过classMethod函数动态添
处理方法一(更多的是为了实现@dynamic属性)
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
} + (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selectorString = NSStringFromSelector(sel); if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
} return [super resolveInstanceMethod:sel];
}
处理方法二(当上一种方法中未做处理时,或处理失败,继续调用下面的方法)
- (id)forwardingTargetForSelector:(SEL)aSelector // 如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,这个对象不能是self自身,否则就会出现无线循环,
一般都调用父类的这个方法来实现返回结果
这一步适合我们将消息转发到另一个能处理该消息的对象上,但是无法对消息进行处理
@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[SUTRuntimeMethodHelper alloc] init];
}
return self;
}
- (void)test {
[self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 将消息转发给_helper来处理
if ([selectorString isEqualToString:@"method2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
处理方法三(上一步还是不能处理消息,启动完整的消息转发机制)
- (void)forwardInvocation:(NSInvocation *)anInvocation //NSInvocation对象 尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数
可以实现一些更复杂的功能,内容修改追回参数等,如果发现某个消息不由本类处理,则调用父类的的同名方法,以便继承体系中每个类都有机会处理此调用请求
必须得重写以下方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
栗子
#import "Monkey.h"
#import "ForwardingTarget.h"
#import <objc/runtime.h> @implementation Monkey - (instancetype)init
{
self = [super init];
if (self) {
_target = [ForwardingTarget new];
[self performSelector:@selector(sel) withObject:@"yeyuyu"];//第一步,找不到这个方法的实现
} return self;
} - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
// //3.第三次机会,调用这个方法,如果返回nil直接崩溃,返回函数签名,则会创建一个对象,执行相应的方法
// id result = [super methodSignatureForSelector:aSelector];
// NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
// result = sig;
// return result; // 3 //第三种情况的第二种写法
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (!sig) {
sig = [ForwardingTarget instanceMethodSignatureForSelector:aSelector];
}
return sig; } - (void)forwardInvocation:(NSInvocation *)anInvocation
{
// //3.返回函数签名后执行这个方法。执行相应的操作
// // [super forwardInvocation:anInvocation];
// anInvocation.selector = @selector(invocationTest);
// [self.target forwardInvocation:anInvocation]; //第三种情况的第二种写法
ForwardingTarget *new = [ForwardingTarget new];
if ([new respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:new];
} } @end #import "ForwardingTarget.h"
#import <objc/runtime.h> @implementation ForwardingTarget - (void)sel
{
//第二被转移到本类中之后会查找本类中是否有这个方法,这个类有就执行相应的方法
NSLog(@"ForwardingTarget");
} - (void)forwardInvocation:(NSInvocation *)anInvocation {
//3.进入这个方法中执行相应的方法
[self performSelector:anInvocation.selector withObject:nil];
// [super forwardInvocation:anInvocation];
} @end
消息转发可以达到类似多继承的效果,处理方法二和三,可以允许一个对象与其他对象建立关系,处理某些未知的消息。但是还是有一些区别,例如有的方法不能够用于转发链respondsToSelector:和isKindOfClass:
如果想让这种消息也看起来像继承,也可以重写这些方法。
小姐:实际开发中很少用到这些机制,但是是有助于我们更多的去了解底层的实现,实际编码中也可以更灵活的使用这些机制,实现一些特殊的功能,如hook操作等。
runtime第三部分方法和消息的更多相关文章
- Objective-C Runtime 运行时之三:方法与消息
基础数据类型 SEL SEL又叫选择器,是表示一个方法的selector的指针,其定义如下: typedef struct objc_selector *SEL; objc_selector结构体的详 ...
- Objective-C Runtime 运行时之三:方法与消息(转载)
前面我们讨论了Runtime中对类和对象的处理,及对成员变量与属性的处理.这一章,我们就要开始讨论Runtime中最有意思的一部分:消息处理机制.我们将详细讨论消息的发送及消息的转发.不过在讨论消息之 ...
- 理解Objective-C Runtime(三)消息转发机制
消息转发机制概述 上一篇博客消息传递机制中讲解了Objective-C中对象的「消息传递机制」.本文需要讲解另外一个重要问题:当对象受到无法处理的消息之后会发生什么情况? 显然,若想令类能理解某条消息 ...
- iOS 高级开发 runtime(三)
三 .动态添加方法 我们可以通过runtime动态地添加方法.那么到底啥叫动态添加方法呢?动态添加方法就是当我们程序运行时才知道我们应该调用哪个方法.我们首先需要了解这一点,当我们编写完一段代码后,我 ...
- iOS runtime探究(三): 从runtime開始理解OC的属性property
你要知道的runtime都在这里 转载请注明出处 http://blog.csdn.net/u014205968/article/details/67639303 本文主要解说runtime相关知识, ...
- iOS runtime探究(二): 从runtime開始深入理解OC消息转发机制
你要知道的runtime都在这里 转载请注明出处 http://blog.csdn.net/u014205968/article/details/67639289 本文主要解说runtime相关知识, ...
- 转载:WinForm中播放声音的三种方法
转载:WinForm中播放声音的三种方法 金刚 winForm 播放声音 本文是转载的文章.原文出处:http://blog.csdn.net/jijunwu/article/details/4753 ...
- mysql分表的三种方法
先说一下为什么要分表当一张的数据达到几百万时,你查询一次所花的时间会变多,如果有联合查询的话,我想有可能会死在那儿了.分表的目的就在于此,减小数据库的负担,缩短查询时间.根据个人经验,mysql执行一 ...
- 利用Objective-C运行时hook函数的三种方法
版权声明:转载请注明出处:http://blog.csdn.net/hursing 方法一,hook已有公开头文件的类: 首先写一个Utility函数: #import <objc/runtim ...
随机推荐
- mac下搭建redis环境
一.redis简介 redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合)和zset(有 ...
- AndroidStudio中make Project、clean Project、Rebuild Project的区别
1.Make Project:编译Project下所有Module,一般是自上次编译后Project下有更新的文件,不生成apk. 2.Make Selected Modules:编译指定的Modul ...
- 1229【MySQL】性能优化之 Index Condition Pushdown
转自http://blog.itpub.net/22664653/viewspace-1210844/ [MySQL]性能优化之 Index Condition Pushdown2014-07-06 ...
- 初步学习border-radius
1.属性解析 border-radius是css3属性,他可以使div的角进行一定程度的弯曲. 比如说下面这个width和height的正方形div 经过设置border-radius之后四个角会出现 ...
- POJ 1149 PIGS
PIGS Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 20579 Accepted: 9387 Description ...
- CodeForces - 261B Maxim and Restaurant
http://codeforces.com/problemset/problem/261/B 题目大意:给定n个数a1-an(n<=50,ai<=50),随机打乱后,记Si=a1+a2+a ...
- 【BZOJ-2179&2194】FFT快速傅里叶&快速傅里叶之二 FFT
2179: FFT快速傅立叶 Time Limit: 10 Sec Memory Limit: 259 MBSubmit: 2978 Solved: 1523[Submit][Status][Di ...
- Java 配置maven及阿里云镜像
一:配置maven 1.下载maven,选择Binary tar.gz,解压拷贝到目录/usr/local/ https://maven.apache.org/download.cgi 2.配置系统默 ...
- 关于push数组,然后遍历数组遇到的坑,遍历显示函数
我偷了个懒将点击的东西push进一个arr里,然后遍历显示在上面. 为啥子出现了上函数,什么鬼什么鬼.我检查很久都不晓得那里push进去的. 一个小时后,我想想要不看看arr里面的结构吧! 尼玛!为啥 ...
- 纯CSS3实现动态导航栏目
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...