runtime介绍
- Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。Runtime源码下载
- 理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。
高级编程语言想要成为可执行文件需先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC
并不能直接编译为汇编语言,而是要先转写为纯C
语言再进行编译和汇编操作,从OC
到C
语言的过渡就是runtime
来实现的。然而我们使用OC
进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
Runtime消息传递
一个对象的方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
,Runtime
时执行的流程是这样的:
- 首先,通过
obj
的isa
指针找到它的class
; - 在
class
的method list
找foo
; - 如果
class
中没到foo
,继续往它的superclass
中找 ; - 一旦找到
foo
这个函数,就去执行它的实现IMP
。
但这种实现有个问题,效率低。但一个class
往往只有 20%
的函数会被经常调用,可能占总调用次数的 80%
。每个消息都需要遍历一次objc_method_list
并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class
中另一个重要成员objc_cache
做的事情 - 再找到foo
之后,把foo
的method_name
作为key
,method_imp
作为value
给存起来。当再次收到foo
消息的时候,可以直接在cache
里找到,避免去遍历objc_method_list
。从前面的源代码可以看到objc_cache
是存在objc_class
结构体中的。
objec_msgSend的方法定义如下:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体:
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
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;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
- 系统首先找到消息的接收对象,然后通过对象的
isa
找到它的类。 - 在它的类中查找
method_list
,是否有selector
方法。 - 没有则查找父类的
method_list
。 - 找到对应的
method
,执行它的IMP
。 - 转发
IMP
的return
值。
点击查看消息传递用到的一些概念:
- 类对象(objc_class)
- 实例(objc_object)
- 元类(Meta Class)
- Method(objc_method)
- SEL(objc_selector)
- IMP
- 类缓存(objc_cache)
- Category(objc_category)
Runtime消息转发
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject
),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:
方法报unrecognized selector
错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。
- 动态方法解析
- 备用接收者
- 完整消息转发
动态方法解析
首先,Objective-C
运行时会调用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。如果你添加了函数并返回YES
, 那运行时系统就会重新启动一次消息发送的过程。
实现一个动态方法解析的例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
打印结果:
2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo
可以看到虽然没有实现foo:
这个函数,但是我们通过class_addMethod
动态添加fooMethod
函数,并执行fooMethod
这个函数的IMP
。从打印结果看,成功实现了。
如果resolve
方法返回 NO
,运行时就会移到下一步:forwardingTargetForSelector
。
备用接收者
如果目标对象实现了-forwardingTargetForSelector:
,Runtime
这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
实现一个备用接收者的例子如下:
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
@end
打印结果:
2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo
可以看到我们通过forwardingTargetForSelector
把当前ViewController
的方法转发给了Person
去执行了。打印结果也证明我们成功实现了转发。
完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。
实现一个完整转发的例子如下:
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
@end
打印结果:
2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo
从打印结果来看,我们实现了完整的转发。通过签名,Runtime
生成了一个对象anInvocation
,发送给了forwardInvocation
,我们在forwardInvocation
方法里面让Person
对象去执行了foo
函数。签名参数v@:
怎么解释呢,这里苹果文档Type Encodings有详细的解释。
以上就是Runtime
的三次转发流程。下面我们讲讲Runtime
的实际应用。
Runtime应用
Runtime
简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。
- 关联对象(Objective-C Associated Objects)给分类增加属性
- 方法魔法(Method Swizzling)方法添加和替换和KVO实现
- 消息转发(热更新)解决Bug(JSPatch)
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换(MJExtension)
网友评论