前言
本篇博文主要以初学者学习 Runtime 的角度介绍,由浅入深同时结合简单的示例,让读者更快捷的学习。【注】若读者对本文中的内容有所疑惑或者不对的地方,请不要吝啬您的留言,让我们一起成长
本文结构
- Runtime 介绍
- OC 类基础结构介绍
- OC 函数是如何调用的
- Runtime 消息转发
Runtime 介绍
-
Runtime 是什么?
Objective-C 拓展了 C 语言,添加了 面向对象特性 和 消息传递机制,拓展的核心是一个用 C 语言和汇编语言写的 Runtime 系统,它是 OC 面向对象和动态机制的基石。
-
Runtime 发展历程
Runtime 有两个版本:“modern” 和 “legacy”,现在OC-2.0采用的是 Modern 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的64位程序中。而 macOS 较老的32位程序仍采用 OC -1中的 Legacy 版本的 runtime 系统。这两版本的区别:当你更改一个类的实例变量的布局时,在早期的版本需要重新编译它的子类。而现版本就不需要。
-
OC 语言动态性的体现
本质体现:一些高级编程语言,像 C/C++ 语言,想要成为可执行文件,需要经过编译(预处理 & 编译),汇编成机器语言。机器语言是计算机唯一能识别的语言,但是 OC 并不能直接编译转换为汇编语言,而是要先转为 C 语言在进行编译和汇编的处理,OC->C语言->在进行编译汇编的操作。从OC到C的过渡就是由Runtime来实现的,将面向对象的类开发转换成面向过程的结构体开发。
这就给动态化留出足够多的舞台,运行时动态的创建类和对象,进行消息传递和转发等等。
-
源代码
Runtime 基本使用 C 和汇编写的。苹果维护了自己的 runtime 源代码。GNU也维护了自己的开源的 runtime 版本,两个版本之间都在尽力保持一直,平时的业务中主要使用官方API,解决框架性的需求。
OC 类基础结构介绍
- 类对象 (objc_class)
- 实例 (objc_object)
- 元类 (Meta Class)
- Method (objc_method)
- SEL (objc_selector)
- IMP
- 类缓存 (objc_cache)
- Category (objc_category)
类对象 (objc_class)
Objective-C 类是由 Class 类型来表示,它实际上是一个指向 objc_class 结构体的指针
typedef struct objc_class *Class;
查看 objc.runtime.h 中的 objc_class 结构体的定义如下:
结构体里面定义了很多变量
- isa
- super_class:指向父类的指针
- name:类的名字
- instance_size:实例大小
- struct objc_ivar_list *ivars:实例变量列表
- struct objc_method_list *methodlists:方法列表
- cache:缓存
- protocols:遵守的协议
类对象其实就是一个结构体 struct objc_class,结构体里面存放的数据成为元数据 (metaclass)。该结构体的第一个成员变量也是 isa 指针,这就说明了 Class 本身也是一个对象,因此我们称为类对象,类对象在编译期间用于创建实例对象,是单例。
实例(objc_object)
/// 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;
元类(Meta Class)
元类是一个类对象的类,元类中保存了创建类对象以及类方法所需的所有信息。任何 NSObject 继承体系下的 meta-class 都使用 NSObject 的 meta-class 作为自己的所属类,而基类的 meta-class 的isa 指针是指向它自己的。
首先我们先举个例子来验证一下
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign)int height;
@end
@implementation Person
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
Person *p1 = [[Person alloc] init];
p1->_age = 3;
Person *p2 = [[Person alloc] init];
p2->_age = 4;
}
return 0;
}
显而易见,p1 和 p2 必然是不一样的,因为他们的存放位置不一样
那么p1 指向的实例对象的isa 指针 和 p2 指向的实例对象的isa 指针式一样的吗?可想而知他们都指向Person类对象,是一样的
Method(objc_method)
先来看一下定义
runtime.h
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
Method
和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:
- (void)logName
{
NSLog(@"name");
}
我们来看下objc_method
这个结构体的内容:
- SEL method_name 方法名
- char *method_types 方法类型
- IMP method_imp 方法实现
SEL
和IMP
其实都是Method
的属性。
SEL(obj_selector)
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;
OC 中调用一个方法 [obj func],编译器会转成消息转发 objc_msgSend(obj, func),其中这个func 就是一个 SEL。从定义中可以发现 SEL 是 selector 在OC 中表示的类型。selector 是方法选择器,可以理解为是区分方法的 ID,而这个 ID 的数据结构就是 SEL
@property SEL selector;
可以看到 selector 是SEL 的一个实例
A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
其实 selector 就是一个映射到方法的 C 字符串,可以用 Objective-C
编译器命令@selector()
或者 Runtime
系统的sel_registerName
函数来获得一个 SEL
类型的方法选择器。
selector 既然是一个 string,可以想象一下这个string 应该是类似 className + method 的组合,这样的话就会产生两条规则
- 同一个类,selector 不能重复
- 不同的类,selector 可以重复
这样其实会产生一个弊端:比如在C 和 C++ 中,会存在函数重载,函数名相同单参数不同,这在OC 中是不行的,因为selector 只记录了 method 的 name,没有参数。
例如
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;
在OC 中只能通过函数命名来区分。
IMP
/// A pointer to the function of a method implementation. 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
#endif
就是指向最终实现程序的内存地址的指针。
OC 函数是如何调用的呢?
上文说过 [obc func] 其实是 objc_msgSend(obj, func),在 Runtime 中执行的流程是:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
- 首先通过 obj 的 isa 指针找到他的 class
- 在 class 的method list 中找 func;
- 如果 class 中没找到 func,继续往 superclass 中找;
- 当找到 这个函数后,就去执行他的 IMP
//对象
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;
}
但这种实现有个问题,效率低。但一个 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
结构体中的。
类缓存(objc_cache)
基于上述的问题,类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend
查找一个类的选择器,它首先搜索类缓存。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache
,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime
系统实际上非常快,接近直接执行内存地址的程序速度。
Runtime 消息转发
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject
),如果还是找不到并且消息转发都失败了要怎么处理呢?那就是进行消息转发:他会有三次机会进行处理
- 动态方法解析
- 备用接收者
- 完整的消息转发
动态方法解析
首先,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 NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
@end
可以看到我们通过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 NO;
}
- (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
从打印结果来看,我们实现了完整的转发。通过签名,Runtime
生成了一个对象anInvocation
,发送给了forwardInvocation
,我们在forwardInvocation
方法里面让Person
对象去执行了foo
函数。签名参数v@:
怎么解释呢,这里苹果文档Type Encodings有详细的解释。
以上就是Runtime
的三次转发流程。
Runtime 的一些应用
- 关联对象(Objective-C Associated Objects)给分类增加属性
- 方法魔法(Method Swizzling)方法添加和替换和KVO实现
- 消息转发(热更新)解决Bug(JSPatch)
- 实现NSCoding的自动归档和自动解档