本文摘抄自iOS 开发:『Runtime』详解(一)基础知识,不做任何商业用途。优秀的作品要大家一起欣赏
本文主要介绍「Runtime」的相关基础知识。主要有一下几点:
1、什么是Runtime?
2、消息机制的基本原理
3、Runtime中的概念解析(objc_msgSend/Class/Object/MetaClass/Method)
4、Runtime消息转发
5、消息发送以及转发机制总结
1、什么是Runtime?
我们都知道,将源代码转换为可执行程序,主要经过三个步骤:编译、链接、运行。不同的编译语言,在这三个步骤中所进行的操作又有一些不同。
C语言
作为一门静态语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了调用的函数,以及函数的实现。
而Objective—C
是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道真正调用的是哪个函数。只有在运行时间才能检查变量的数据类型,同时在运行时才会根据函数名
查找要调用的具体函数。这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么。
Objective-C
把一些决定性的工作从编译阶段
、链接阶段
推迟到运行时阶段
的机制,使得Objective-C
变得更加灵活。我们甚至可以在程序运行的时候,动态去修改一个方法的实现,这也为「热更新」提供了可能性。
而实现Objective-C
运行时机制的一切基础就是Runtime
。
Runtime
实际上是一个库,这个库使我们可以在程序运行的时候,动态的创建对象、检查对象,修改类和对象的方法。
2、消息机制的基本原理
Objective-C
中,对象方法的调用都是类似[receicer selector];
的形式,其本质就是让对象在运行的时候发送消息的过程。
我们来看看方法调用[receiver selector];
在「编译阶段」和「运行阶段」分别做了什么?
-
编译阶段:
[receiver selector];
方法被编译器转换为:-
objc_msgSend(receiver, selector)
--- (不带参数) -
objc_msgSend(receiver, selector, org1, org2, ...)
--- (带参数)
-
-
运行时阶段:消息接受者
receiver
寻找对应的selector
.- 通过
receiver
的isa 指针
找到receiver
的Class (类)
; - 在
Class (类)
的cache (方法缓存)
的散列表中寻找对应的IMP (方法实现)
; - 如果在
cache (方法缓存)
中没有找到对应的IMP (方法实现)
的话,就继续在Class (类)
的method list (方法列表)
中找对应的selector
,如果找到,填充到cache (方法缓存)
中,并返回selector
; - 如果在
Class (类)
中没有找到这个selector
,就继续在它的superClass (父类)
中寻找; - 一旦找到对应的
selector
,直接执行receiver
对应的selecotr
方法实现的IMP (方法实现)
。 - 若找不到对应的
selector
,消息被转发 或者 临时向receiver
添加这个selector
对应的实现方法,否则就会发生崩溃。
- 通过
在上述过程中,涉及了好几个概念:objc_msgSend
、isa 指针
、Class (类)
、IMP (方法实现)
等,下面我们来具体探讨一下这些概念。
3、Runtime中的概念解析
3.1、objc_msgSend
所有的Objective-C
方法调用,在编译的时候会转化为C函数objc_msgSend
的调用。
objc_msgSend(receiver, selector);
是[receiver selector];
对应的 C函数。
3.2、Class(类)
在objc/runtime.h
中,Class (类)
被定义为指向Objc_class 结构体
的指针,Objc_class 结构体
的数据结构如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // objc_class 结构体的实例指针
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; // 指向父类的指针
const char * _Nonnull name OBJC2_UNAVAILABLE; // 类的名称
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为 ·0·
long info OBJC2_UNAVAILABLE; // 类的信息,共运行时使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 该类的实例变量列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法定义的列表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 遵守的协议列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
从源码中我们可以看出,
objc_class 结构体
定义了很多变量:自身的所有实例变量(ivars)、所有方法的定义(methodLists)、遵守的协议列表(protocols)等。
objc_class 结构体
存放的数据称为:元数据(metadata)。
objc_class 结构体
的第一个成员变量是isa 指针
;isa 指针
保存的是所属类的结构体的实例指针,这里保存的就是objc_class 结构体
的实例指针,而实例换个名字就是对象。换句话说,Class (类)
的本质就是一个对象,我们称之为:类对象。
3.3、Object(对象)
接下来,我们再来看看objc/objc.h
中关于Object (对象)
的定义。
Object (对象)
被定义为objc_object 结构体
,其数据结构如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // objc_object 结构体的实例指针
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
这里的id
被定义为一个指向objc_object 结构体
的指针。从中可以看出objc_object 结构体
只包含一个Class
类型的isa 指针
。
也就是说,一个Object (对象)
唯一保存的就是它所属Class (类)
的地址。当我们对一个对象进行方法调用的时候,比如[receiver selector];
,它会通过objc_object 结构体
的isa 指针
去找到对应的object_class 结构体
,然后在object_class 结构体
的methodLists (方法列表)
中找到我们调用的方法,然后执行。
3.4、Meta Class(元类)
从上面的内容我们看出,对象 (objc_object 结构体)
的isa 指针
指向的是对应的类对象 (object_class 结构体)
。那么类对象 (object_class 结构体)
的isa 指针
又指向哪里呢?
object_class 结构体
的isa 指针
实际上指向的是类对象
自身的Meta Class (元类)
。
Meta Class (元类)
就是一个类对象所属的类
。一个对象所属的类叫做类对象
,而一个类对象所属的类就叫做元类
。
Runtime中,把类对象的所属类型叫做
Meta Class (元类)
,用于描述类对象本身所具有的特征,而在元类
的methodLists
中,保存了类的方法链表,即所谓的「类方法」。并且类对象中的isa 指针
指向的就是元类
。每个类对象有且仅有一个与之相关的元类
。
在上面2、消息机制的基本原理中,我们探讨了对象方法的调用流程,我们通过对象的isa 指针
找到对应的Class (类)
;然后在Class (类)
的methodLists (方法列表)
中找到对应的selector
。
而类方法的调用流程和对象方法的调用流程是差不多的,流程如下:
- 通过类对象
isa 指针
找到所属的Meta Class (元类)
; - 在
Meta Class (元类)
的methodLists (方法列表)
中找到对应的selector
; - 执行对应的
selector
。
下面看一个示例:
NSString *str = [NSString stringWithFormat:@"%d", 1];
示例中,stringWithFormat:
被发送给了NSString 类
,NSString 类
通过isa 指针
找到NSString 元类
,然后在该元类的方法列表中找到对应的stringWithFormat:
方法,然后执行该方法。
3.5、示例对象、类、元类之间的关系
上面,我们讲解了实例对象 (Object)
、类 (Class)
、Meta Class (元类)
的基本概念,以及简单的指向关系。下面我们通过一张图来捋一下它们之间的关系。
我们先来看
isa 指针
:
- 水平方向上,每一级中的
实例对象
的isa 指针
指向了对应的类对象
,而类对象
的isa 指针
指向了对应的元类
。而所有元类
的isa 指针
最终指向了NSObject 元类
,因此NSObject 元类
也被称为根元类
。 - 垂直方向上,
元类
的isa 指针
和父类元类
的isa 指针
都指向了根元类
。而根元类
的isa 指针
又指向了自己。
我们再来看父类指针
:
-
类对象
的父类指针
指向了父类的类对象
;父类的类对象
又指向了根类的类对象
,跟类的类对象
最终指向了nil
。 -
元类
的父类指针
指向了父类对象的元类
;父类对象的元类
的父类指针
指向了根类对象的元类
,也就是根元类
;而根元类
的父类指针
指向了根类对象
,最终指向了nil
。
3.6、Method(方法)
object_class 结构体
的methodLists (方法列表)
中存放的元素是Method (方法)
。
我们来看一下objc/runtime.h
中,表示Method (方法)
的objc_method 结构体
的数据结构:
/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实现
}
可以看到,objc_method 结构体
中包含了method_name (方法名)
、method_types (方法类型)
和method_imp (方法实现)
。下面我们来详细探索一下这三个变量。
-
SEL method_name;
--- 方法名
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
SEL
是一个指向objc_selector 结构体
的指针,但是Runtime
相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL
只是一个保存方法名的字符串。
-
IMP method_imp;
--- 方法实现
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP
的实质是一个函数指针,所指向的就是方法的实现。IMP
用来找到函数地址,然后执行函数。
-
char * method_types;
--- 方法类型
方法类型method_types
是个字符串,用来存储方法的参数类型和返回值类型 。
探索到这里,
Method
的结构已经很明朗了。
Method
将SEL (方法名)
和IMP (函数指针)
关联起来;当对一个对象发送消息时,会通过给出SEL (方法名)
去找到IMP (函数指针)
,然后执行。
4、Runtime 消息转发
在上面消息发送流程中我们提到过:若找不到对应的selector
,消息被转发或者临时向receiver
添加selector
对应的方法实现,否则就会发生崩溃。
当一个方法找不到的时候,Runtime提供了 消息动态解析、消息接受者重定向、消息重定向等三步处理消息。具体流程如下:
4.1、消息动态解析
Objective-C
运行时会调用+resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。前者
在对象方法未找到时调用,后者
在类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES
,那运行时系统就会重新启动一次消息发送的过程。
主要使用的方法如下:
/**
* Adds a new method to a class with a given name and implementation.
*
* @param cls The class to which to add a method.
* @param name A selector that specifies the name of the method being added.
* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
* @param types An array of characters that describe the types of the arguments to the method.
*
* @return YES if the method was added successfully, otherwise NO
* (for example, the class already contains a method implementation with that name).
*
* @note class_addMethod will add an override of a superclass's implementation,
* but will not replace an existing implementation in this class.
* To change an existing implementation, use method_setImplementation.
*/
/**
* class_addMethod 向具有给定 名称和实现 的类中添加新方法
* @param cls 被添加方法的类
* @param name selector 方法名
* @param imp 实现方法的函数指针
* @param types 描述方法参数类型的字符数组
* @return
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
eg:
#import "ViewController.h"
#include "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self performSelector:@selector(func)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(func)) {
class_addMethod([self class], sel, (IMP)funcMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void funcMethod(id objc, SEL _cmd) {
NSLog(@"new funcMethod");
}
@end
输出结果:
2021-05-25 10:16:58.373018+0800 test[22870:647107] new funcMethod
从上面的例子中,我们可以看出,虽然我们没有实现func
方法,但是通过重写resolveInstanceMethod:
,利用class_addMethod
方法添加对象方法实现funcMethod
方法,并执行。从打印的结果来看,我们成功调起了funcMethod
方法。
大家也注意到了
class_addMethod
中types
这个参数的传入比较特殊。这里大家可以参考官方文档中关于Type Encodings
的说明。「官方文档」
4.2、消息接受者重定向
如果上一步中+resolveInstanceMethod:
或者 +resolveClassMethod:
没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。
如果当前对象实现了-forwardingTargetForSelector:
或者 +forwardingTargetForSelector:
方法,·Runtime·就会调用这个方法,允许我们将消息的·接受者·转发给其他对象。
其中用到的方法如下:
///重定向类方法的消息接受者,返回一个类或实例对象
+ (id)forwardingTargetSelector:(SEL)aSelector;
///重定向实例方法的消息接受者,返回一个类或实例对象
- (id)forwardingTargetSelector:(SEL)aSelector;
注意:
1、类方法 和 实例方法,所对应的·消息接受接重定向·,使用的是两个不同的方法,一个是+
,一个是-
。
2、这里+resolveInstanceMethod:
或者+resolveClassMethod:
无论返回值是YES
还是NO
,只要其中没有添加其他函数的实现,运行时都会进行下一步。
eg:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)func;
@end
@implementation Person
- (void)func {
NSLog(@"Person ---- func");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self performSelector:@selector(func)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;
}
///消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(func)) {
return [[Person alloc] init]; ///返回Person对象,让Person对象去接受这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
@end
打印结果:
2021-05-25 10:51:27.007218+0800 test[23042:684421] Person ---- func
可以看到,虽然当前ViewController
没有实现func
方法,+resolveInstanceMethod:
也没有添加其他函数实现。但是我们通过forwardingTargetSelector
把当前ViewController
的方法转发给了Person
对象去执行了。打印结果也证明了我们成功实现了转发。
我们通过forwardingTargetSelector
可以修改消息的接受者,该方法返回参数是一个对象,如果这个对象不是nil
,也不是self
,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向
4.3、消息重定向
如果经过消息动态解析、消息接受者重定向,Runtime
系统还是找不到相应的方法实现,从而无法相应消息,Runtime
系统会利用-methodSignatureForSelector:
或者 +methodSignatureForSelector:
方法获取函数的参数和返回值类型。
- 如果
methodSignatureForSelector
返回了一个NSMethodSignature
对象(函数签名),Runtime
系统就会创建一个NSInvocation
对象,并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找IMP
的机会。 - 如果
methodSignatureForSelector
返回nil
。则Runtime
系统会发出doesNotRecognizeSelector
消息,程序也就崩溃了。
所以我们可以在forwardInvocation
方法中对消息进行转发。
注意:类方法 和 对象方法 消息转发第三步调用的方法同样不一样。
类方法的调用:
1、+ methodSignatureForSelector:
2、+ forwardInvocation:
3、+ doesNotRecognizeSelector:
对象方法的调用:
1、- methodSignatureForSelector:
2、- forwardInvocation:
3、- doesNotRecognizeSelector:
用到的方法:
///获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
///对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
///获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("")
///类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
eg:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)func;
@end
@implementation Person
- (void)func {
NSLog(@"Person ---- func");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self performSelector:@selector(func)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; ///为了进行下一步 --> 消息接受者重定向
}
///消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; ///为了进行下一步 ---> 消息重定向
}
///获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"func"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
///消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector; ///从 anInvocation 中获取消息
Person *p = [[Person alloc] init];
if ([p respondsToSelector:sel]) { ///判断 Person 对象方法是否可以响应 sel
[anInvocation invokeWithTarget:p]; ///若可以响应,则将消息转发给其他对象处理
} else {
[self doesNotRecognizeSelector:sel]; ///若仍然无法响应,则报错,找不到响应方法
}
}
@end
打印结果:
2021-05-25 13:36:53.710918+0800 test[23878:778445] Person ---- func
可以看到,我们在-forwardInvocation:
方法里面让Person
对象执行了func
函数。
既然-forwardingTargetForSelector:
和 -forwardInvocation:
都可以将消息转发给其他对象处理,那么两者之间的区别是什么?
区别在于-forwardingTargetForSelector:
只能将消息转发给一个对象;而-forwardInvocation:
可以将消息转发给多个对象。
以上就是Runtime
消息转发的整个流程。
5、消息发送以及转发机制总结
调用[receiver selector];
后,进行的流程:
-
编译阶段:
[receiver selector];
方法被编译器转换为:-
Objc_msgSend(receiver, selector)
--- 不带参数 -
Objc_msdSend(receiver, selector, org1, org2, ...)
--- 带参数
-
-
运行时阶段:消息接受者
receiver
寻找对应的selector
- 通过
receiver
的isa 指针
找到receiver
的Class (类)
; - 在
Class (类)
的cache (方法缓存)
的散列表中寻找对应的IMP (方法实现)
; - 如果在
cache (方法缓存)
中没有找到对应的IMP (方法实现)
的话,就继续在Class (类)
的method list (方法列表)
中找对应的selector
,如果找到,填充到cache (方法缓存)
中,并返回selector
; - 如果在
class (类)
中没有找到这个selector
,就继续在它的superclass (父类)
中寻找; - 一旦找到对应的
selector
,直接执行receiver
对应的selector
方法实现的IMP (方法实现)
。 - 若找不到对应的
selector
,Runtime
系统进入消息转发机制。
- 通过
-
运行时消息转发阶段:
- 动态解析:通过重写
+resolveInstanceMethod:
或者+resolveClassMethod:
方法,利用class_addMethod
方法添加其他函数实现; - 消息接受者重定向:如果上一步没有添加其他函数实现,可在当前对象中利用
forwardingTargetForSelector:
方法将消息的接受者转发给其他对象; - 消息重定向:如果上一步返回值是
nil
,则利用methodSignatureForSelector:
方法获取函数的参数和返回值类型。- 如果
methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime
系统就会创建一个NSInvocation
对象,并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找IMP
的机会。 - 如果
methodSignatureForSelector:
返回nil
。则Runtime
系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。
- 如果
- 动态解析:通过重写
比如说上面的第3步,我们可以制造奔溃,然后通过函数调用栈来查看一下: