Runtime之二 消息机制(传递和转发)
先看一个栗子
id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];
上述代码在编译期没有任何问题
,因为id类型
可以指向任何类型的实例对象,在编译期不确定这个num到底具体指代什么类型
的实例对象,并且在运行期还可以给NSNumber类型添加新的方法.
NSString有appendString:方法
,因此编译期发现有appendString:的函数声明就不会报错,但在运行时在NSNumber类中找不到appendString:方法
就会报错。
这也就是消息传递的强大之处和弊端,编译期无法检查到未定义的方法,运行期可以动态添加新的方法。
在OC中的实例对象调用一个方法称作消息传递,消息传递采用动态绑定机制来决定具体调用哪个方法,因为编译期根本不能确定最终会调用哪个方法,因为运行期可以动态修改方法的实现
从Runtime角度讲解OC的消息传递和消息转发机制
第一、发送一个消息底层到底经历了那些过程
比如向一个OC实例对象发送一个消息:
[person say:@"hello"];
person称为消息的接受者
say:称为选择子 (就是常用的selector)selector和参数共同构成了消息
方法的调用在运行时最终会被翻译成objc_msgSend的函数调用:
objc_msgSend(person, selector);
如果有参数,则可能是:
objc_msgSend(person , selector, arg1, arg2, …);
我们知道OC并不是在编译期决定调用哪个函数
,而是在运行期
采用动态绑定机制来决定具体调用哪个方法,因此可以在运行期去动态修改方法的实现。
编译阶段只是确定了要向person对象发送say消息,因为编译阶段完全不知道say方法的具体实现(甚至该方法到底有没有被实现也不知道),所有没有发送,真正发送是等到运行的时候进行。
所谓的“runtime”实际上就是一个管理运行代码的环境机制
保证了代码在运行中有自我检查,判断的能力。
比如下面的代码段:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
- (void)showMyself;
@end
@implementation Person
- (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 = @"Jia Jung";
[p showMyself];
}
return 0;
}
通过clang命令
可以看到翻译后代码如下:
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;
}
可以看到 [Person alloc];
该方法的调用代码在运行时可简要表示为: objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
这一行代码其实做了三件事情
第一获取消息的接收者 -》 Person的类对象
第二注册alloc消息 -》 获取到函数的函数指针 通过函数指针可以最终可以找到方法的实现
第三发送消息 -》 将alloc消息发送消息的接受者
可以看到[p init]
该方法的调用简写为如下:objc_msgSend(p, sel_registerName("init"));
将init消息
发送给消息的接受者p
接着是一个对setter的调用,同样的也可以简写为如下代码:
//这一行是用来查找参数的地址,取名为name
(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1)
objc_msgSend(p, sel_registerName("setName:"), name);
同理,最后一行代码也可以简写为如下:
objc_msgSend(p, sel_registerName("showMyself"));
到这里知道了方法调用底层会转换成objc_msgSend函数
调用; objc_msgSend函数
根据消息的接受者和selector选择子 然后选择适当的方法去调用;那么它又是如何选择到底哪个方法是合适的呢?
说明:
之前我们知道了对象、类对象、元类的概念,也知道objc_class
这个结构体里面有一个成员变量methodLists
- 类对象的
methodLists
里面保存了对象方法
的列表;当我们调用对象方法时,runtime
会在类对象的方法列表中
查找方法 - 元类对象的
methodLists
里面保存了类方法
的列表;当我们调用类方法时,runtime
会在元类对象的方法列表中
查找方法
当我们调用对象方法的时候,会根据selector选择子去到“类对象”的方法列表里去找,我们知道elector选择子在底层被转换成一个函数指针,通过这个函数指针去找对应的 IMP方法的实现
当我们调用类方法的时候,会根据selector选择子去到“元类对象”的方法列表里去找,我们知道elector选择子在底层被转换成一个函数指针,通过这个函数指针去找对应的 IMP方法的实现
查看发现方法列表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;
};
方法列表 objc_method_list
的几个成员变量:
entsize -》结构体 _objc_method 的大小
method_count -》方法个数
-
method_list 方法列表数组 里面存放的是
struct _objc_method
结构体类型的数据-
_objc_method
这个结构体里保存了选择子_cmd的指针
、方法类型、方法实现的_imp指针
- 可以看出方法的具体实现也是一个函数指针,也就是我们自定义的实例方法,选择子也就是selector可以理解为是一个字符串类型的名称,用于查找对应的函数实现(由于苹果没有开源selector的相关代码,但是可以查到GNU OC中关于selector的定义,也是一个结构体但是结构体里存储的就是一个字符串类型的名称)。
-
SEL:类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。
IMP:一个函数指针,保存了方法的地址
Method:方法的结构体,其中保存了方法的名字,实现和类型描述字符串
到这里我们完美的解释了一个方法在底层是如果找到方法的具体实现调用的,也就是objc_msgSend的工作原理
。
第二、 问题又来了,假如方法的实现没有找到底层又会发生什么??
如果没有找到方法实现,就会通过super_class指针
沿着继承树向上去查找,如果找到就执行,如果到了继承树的根部(通常为NSObject)还没有找到,那就会调用NSObjec的一个方法doesNotRecognizeSelector
报unrecognized selector
错误;
每次发送一个消息需要经过这么多步骤,难道不会影响性能吗?
当然了,这样一次次搜索和静态绑定 和直接跳转到函数指针指向的位置
去执行 比肯定是耗时很多的,因此,objc_class
中有一个成员变量struct objc_cache
这个缓存里缓存的正是搜索方法的匹配结果,这样在第二次及以后再访问时就可以采用映射的方式找到相关实现的具体位置。
接着上面的话题说: 其实在调用doesNotRecognizeSelector
方法之前运行时会进行消息转发
,也就是说在报错之前我们还是有机会去补救的,那么运行时怎么进行消息转发
的,我们又怎么去补救那??
调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
第一次机补救会: 所属类动态方法解析
+(BOOL)resolveInstanceMethod:(SEL)name 动态实现对象方法时在该方法里处理
+ (BOOL)resolveClassMethod:(SEL)name 动态实现类方法时在该方法里处理
如果沿继承树到根部都没有寻找到相关方法实现 ,然后会向接收者所属的类
进行一次请求,看有没有动态添加了一个方法去补救;会去看resolveInstanceMethod 或 resolveClassMethod
这个类方法里面有没有动态的添加一个方法
如果有做动态添加方法的处理,然后返回YES,如果没有处理返回NO (该默认返回NO)
那么我们怎么再resolveInstanceMethod
方法里去动态添加方法实现那?? 举个栗子
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
//如果需要传参直接在参数列表后面添加就好了
void dynamicAdditionMethodIMP(id self, SEL _cmd) {
NSLog(@"dynamicAdditionMethodIMP");
}
+ (BOOL)resolveInstanceMethod:(SEL)name {
if (name == @selector(appendString:)) {
// 通过运行时方法动态添加方法实现
class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name {
return [super resolveClassMethod:name];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
id p = [[Person alloc] init];
[p appendString:@""];
}
return 0;
}
看一下main函数
创建了一个Person的实例对象,一定要用
id类型
来声明,否则会在编译期就报错。(因为找不到相关函数的声明,id类型
由于可以指向任何类型的对象,因此编译时能够找到NSString类
的相关方法声明就不会报错)Person类没有声明和实现
appendString方法
执行时应该会报unrecognized selector错误,但是并没有因为重写了
resolveInstanceMethod
,当找不到appendString方法
时会调用resolveInstanceMethod
询问是否有动态添加,如果有并返回True就会再次执行相关方法,因为我们处理了所以不会报错
如何给一个类动态添加一个方法
// 通过class_addMethod方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
- 第一个参数 需要添加方法的类
- 第二个参数 是一个selector 也就是方法的名字可以随意起
- 第三个参数 是一个IMP类型的变量 也就是函数的实现 (自己实现一个C函数,该函数至少两个参数,一个
id self
一个SEL _cmd
) - 第四个参数
types
定义该函数返回值类型和参数类型(依次按序输入)的字符串(不是NSString类型,而是const char *类型 所以不要用@“”,而要直接用””) 我们上面的函数是void dynamicMethod(id self, SEL _cmd)
返回值是void—>(对应)v
; 第一个参数是self是对象—>对应@
;第二个参数是SEL—》对应:
所以连起来就是“v@:”
。
第二次补救机会: 备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector;
如果并没有在resolveInstanceMethod
方法里动态的实现,那么runtime就会接着询问当前的接受者
是否有其他对象可以处理这个未知的selector
;此时runtime就会看forwardingTargetForSelector方法里
有没有处理
- 这是一个实例方法,该方法的参数就是那个未知的selector
- 询问该实例对象是否有其他实例对象可以接收这个未知的selector,如果没有就返回nil,如果有就返回那个对象
举个例子
在ViewController中调用方法secondVCMethod方法
这样调用肯定会找不到方法而崩溃,下面我们是用forwardingTargetForSelector方法
来转发一下消息
新建一个类SecondViewController
,在它里面有一个secondVCMethod方法
,但是ViewController
和 SecondViewController
这两个类并没有继承关系,正常是无法调用的
继续处理ViewConreoller类,用forwardingTargetForSelector方法来转发一下消息
我们会发现secondVCMethod方法执行了,程序并没有崩溃,原因在于当没有找到secondVCMethod这个方法的时候消息一直传递到方法
- (id)forwadingTargetForSelector:(SEL)aSelector
然后在里面创建了一个SecondViewController的对象,并判断如果这个需要转发的方法是secondViewController中的方法就返回secondViewController的对象,消息成功转发给secondViewController的对象,并执行。同时也相当于完成了一个多继承。
第三次补救机会:消息重定向
- (void)forwardInvocation: (NSInvocation*)invocation;
当没有备援接收者时,就只剩下最后一次机会,那就是消息重定向。这个时候runtime会将未知消息的所有细节都封装为NSInvocation对象
然后调用forwardInvocation方法
这个方法如果没有处理,就会调用父类的相关补救方法(一直到NSObject),如果都没有任何补救处理就会调用doesNotRecognizeSelector:方法
抛出异常。
问题 [super class] 和 [self class] 的问题
详细答案参看iOS经典讲解之[self class] 和 [super class]的区别
在一个类中调用[super class]
很多人认为输出父类的名字,但结果却是与[self class]
一样的输出,不理解这是为什么??
- 其实self是类的隐藏参数 -》指向当前调用方法的类
- 另一个隐藏参数是_cmd 代表当前方法的selector
-
super
并不是一个隐藏参数,它是一个“编译器指示符” -
[self class]和[super class]
接收class消息的都是指向当前类的指针(self和super指向相同的消息接受者) - 其实self和super最后调用的都是NSObject定义的方法,输出本类的类名,所以才会出现上面那种结果
得到一个方法地址
绕过动态绑定的唯一方式就是得到方法的地址
并像调用函数一样调用它。这在极少数场合是合适的,当一个特定的方法被连续调用多次的时候而且你想避免方法每次被执行发送消息的开销。
一个定义在NSObject类中的方法,methodForSelector:你可以要求一个指向实现一个方法的过程的指针,然后用指针调用这个过程,methodForSelector:返回的指针必须仔细转换到恰当的函数类型。返回值和参数类型都应该包含在转换中。
下面的例子展示了setFilled:方法的实现过程如何被调用:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
第一次被传过去的两个参数是接收对象self和方法选择器_cmd。
这些参数在方法语法中被隐藏,但是当方法被当做函数调用的时候必须显示的传递。
使用methodForSelector:来规避动态绑定节约了大量发送消息所需的时间,要使节省变得有意义,必须当一个特定的消息重复很多次的时候,就像上面for循环展示的那样。
参考文章
iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制
Runtime笔记(官方Doc翻译+原创)