Runtime 是什么?
Runtime 是 Objective-C 区别于 C 语言这样的静态语言的一个非常重要的特性。对于 C 语言,函数的调用会在编译期就已经决定好,在编译完成后直接顺序执行。但是 OC 是一门动态语言,函数调用变成了消息发送,在编译期不能知道要调用哪个函数。所以 Runtime 无非就是去解决如何在运行时期找到调用方法这样的问题。
实例对象中存放 isa 指针以及实例变量,有 isa 指针可以找到实例对象所属的类对象 (类也是对象,面向对象中一切都是对象),类中存放着实例方法列表,在这个方法列表中 SEL 作为 key,IMP 作为 value。 在编译时期,根据方法名字会生成一个唯一的 Int 标识,这个标识就是 SEL。IMP 其实就是函数指针 指向了最终的函数实现。整个 Runtime 的核心就是 objc_msgSend 函数,通过给类发送 SEL 以传递消息,找到匹配的 IMP 再获取最终的实现。
类中的 super_class 指针可以追溯整个继承链。向一个对象发送消息时,Runtime 会根据实例对象的 isa 指针找到其所属的类,并自底向上直至根类(NSObject)中 去寻找 SEL 所对应的方法,找到后就运行整个方法。
metaClass是元类,也有 isa 指针、super_class 指针。其中保存了类方法列表。
IMP 可以理解为函数指针,指向了最终的实现。
SEL 与 IMP 的关系非常类似于 HashTable 中 key 与 value 的关系。OC 中不支持函数重载的原因就是因为一个类的方法列表中不能存在两个相同的 SEL 。但是多个方法却可以在不同的类中有一个相同的 SEL,不同类的实例对象执行相同的 SEL 时,会在各自的方法列表中去根据 SEL 去寻找自己对应的IMP。这使得OC可以支持函数重写。
消息传递机制
如下用于描述 objc_msgSend 函数的调用流程:
- 检测 SEL 是否应该被忽略
- 检测发送的 target 是否为 nil ,如果是则忽略该消息
- 当调用实例方法时,通过 isa 指针找到实例对应的 class 并且在其中的缓存方法列表以及方法列表中进行查询,如果找不到则根据 super_class 指针在父类中查询,直至根类(NSObject 或 NSProxy)
- 当调用类方法时,通过 isa 指针找到实例对应的 metaclass 并且在其中的缓存方法列表以及方法列表中进行查询,如果找不到则根据 super_class 指针在父类中查询,直至根类(NSObject 或 NSProxy)
- 如果还没找到则进入消息动态解析过程。
动态消息解析
- 1.通过 resolveInstanceMethod 或者 resolveClassMethod 得知方法是否为动态添加,YES则通过 class_addMethod 动态添加方法,处理消息,否则进入下一步。
- 2.这步会进入 forwardingTargetForSelector 用于指定哪个对象来响应消息。如果返回nil 则进入第三步。这种方式把消息原封不动地转发给目标对象,有着比较高的效率。如果不能自己的类里面找到替代方法,可以重载这个方法,然后把消息转给其他的对象。
- 3.这步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil 说明消息无法处理并报错 unrecognized selector sent to instance,如果返回 methodSignature,则进入 forwardInvocation ,在这里可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果依然不能正确响应消息,则报错 unrecognized selector sent to instance。
实战:消息转发
下面有一段因为没有实现方法而会导致奔溃的代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor whiteColor]];
self.title = @"Test2ViewController";
//实例化一个button,未实现其方法
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(50, 100, 200, 100);
button.backgroundColor = [UIColor blueColor];
[button setTitle:@"消息转发" forState:UIControlStateNormal];
[button addTarget:self
action:@selector(doSomething)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
为解决这个问题,可以专门创建一个处理这种问题的分类:
#import "NSObject+CrashLogHandle.h"
@implementation NSObject (CrashLogHandle)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
//方法签名
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}
@end
因为在category中复写了父类的方法,会出现警告。
解决办法就是在Xcode的Build Phases中的资源文件里,在对应的文件后面 -w ,忽略所有警告。
再次终结下:
-
在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
-
如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息;
-
如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
-
如果没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。
网友评论