Runtime合集
1. 什么是Runtime
Runtime是一个库,位于usr/include/objc, 经常用的api位于该库下的runtime.h
文件中,在使用时需要引用头文件#import <objc/runtime.h>
2. Runtime 做什么用
通过Runtime,我们可以在App运行期动态的创建对象、检查对象、修改类、对象的方法,可以说Runtime是Objective-C的运行时机制的基础
3. 消息机制的基本原理
声明一个Person
类, 类包含两个对象方法(此处为了编译后查找代码方便,我把函数名命为personSleep
,此处不符合代码命名规范请忽略)
@implementation Person
- (void)eatFood:(NSString *)foodName {
NSLog(@"person eat food : %@", foodName);
}
- (void)personSleep {
NSLog(@"person is sleeping...");
}
@end
复制代码
我们在外界调用eatFood
,编译成cpp查看
Person *person = [[Person alloc] init];
[person eatFood:@"baozi"];
[person personSleep];
复制代码
cpp
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("eatFood:"), (NSString *)&__NSConstantStringImpl__var_folders_44_1ht3l6g55dv59_5s62wsv_bm0000gn_T_ViewController_88ee85_mi_0);
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("personSleep"));
复制代码
把代码简化一下,我们可知编译后的[person eatFood]变成了
objc_msgSend(reciver, Selector)
objc_msgSend(reciver, Selector, org1, org2, …)
运行期阶段:消息接收者reciver
寻找Selector
去执行
- 通过
reciver
的isa
指针找到revicer
的Class
- 在
Class
的cache(方法缓存)
的散列表寻找对应的IMP(方法实现)
- 如果2.没找到,就继续在
Class
的method list(方法列表)
中找对应的selector
,如果找到了,填充到Class的cache
中并返回selector
- 如果3.没找到,就继续在其父类中找
- 一旦找到对应的
selector
,直接执行reciver
的selector
的IMP(方法实现)
- 若找不到对应的
selector
,需要消息被转发或临时向这个reciver
添加selector
,否则会发生崩溃
4.Runtime中的概念
4.1 objc_msgSend
所有的Objective-C方法编译后都会变成对objc_msgSend
的调用,
4.2 Class
struct objc_class {
Class _Nonnull isa; //objc_class结构体的实例指针
#if !__OBJC2__
Class _Nullable super_class; //指向父类的指针
const char * _Nonnull name; //类的名称
long version; //类的版本信息,默认为0
long info; //类的信息,供运行期使用的一些位标识
long instance_size; //该类的实例变量大小
struct objc_ivar_list * _Nullable ivars; //该类的实例变量列表
struct objc_method_list * _Nullable * _Nullable methodLists;//方法定义的列表
struct objc_cache * _Nonnull cache; //方法缓存;
struct objc_protocol_list * _Nullable protocols; //遵守的协议列表;
#endif
}
复制代码
从中可以看出,
objc_class
结构体 定义了很多变量:自身的所有实例变量(ivars)
、所有方法定义(methodLists)
、遵守的协议列表(protocols)
等。objc_class
结构体 存放的数据称为元数据(metadata)
。
objc_class
结构体的第一个成员变量是isa
指针,isa
指针保存的是所属类的结构体的实例的指针,这里保存的就是objc_class
结构体的实例指针,换个名字就是对象,也就是说,Class的本质就是一个对象,我们称为 类对象
4.3 Object
在objc.h中, Object
被定义成了objc_class
结构体
/// Represents an instance of a class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa; //objc_object 结构体的实例指针
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
复制代码
从中可以看出,objc_object结构体只包含了一个Class类型的isa指针,也就是说一个Object(对象)唯一保存的就是它所属Class(类)的地址,当我们对一个对象进行方法调用时,比如[receiver selector], 它会通过objc_object结构体的isa指针去找到对应的object_class结构体,然后在object_class结构体的methodLists中找到我们调用的方法,然后执行
4.4 Meta Class
从上边我们能看出,对象的(objc_object结构体)
的isa指针指向对应类对象(object_class结构体)
,那么类对象(object_class结构体)
的isa指针又指向什么呢
object_class结构体
的isa指针实际上指向的是类对象自身的meta-class(元类)
元类就是一个类对象所属的类。一个对象所属的类叫做类对象,一个类对象所属的类就是元类
Runtime中把类对象所属类型叫做
meta-class(元类)
,用于描述类对象本身所具有的特征,而在元类的methodLists中,保存了类的方法列表,即所谓的类方法
,并且类对象中的isa指针指向的就是元类,每个类对象有且仅有一个与之相关的元类
在3. 消息机制的基本原理中讲到,对象的调用过程,是通过对象的isa指针找到类对象,在类对象的methodLists中找到对应的selector
而类方法的调用过程与对象的调用差不多,流程如下:
- 通过
类对象
的isa指针
找到所属的meta-class(元类)
- 在
meta-class
的methodLists
中找到对应的selector
- 执行对应的
selector
下面看一个示例:
NSString *str = [NSString stringWithFormat:@"%d,%s", 3. @"test"];
复制代码
上边的示例中,stringWithFormat
被发送给了NSString
类,NSString
类通过isa
指针找到NSString的元类
,然后在该元类的方法列表中找到对应的stringWithFormat:
方法,然后执行该方法
4.5 实例对象、类、元类的关系
iOS – isa、superclass指针,元类superclass指向基类本身
4.6 Method
object_class结构体
中的methodLists(方法列表)
中存放的元素就是Method(方法)
objc/runtime.h
中,表示Method的‘objc_method结构体’数据结构如下
struct objc_method {
SEL _Nonnull method_name; //方法名
char * _Nullable method_types; //方法类型
IMP _Nonnull method_imp; //方法实现
}
复制代码
- SEL method_name 方法名
SEL的定义在objc/objc.h
中
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
复制代码
SEL是一个指向objc_selector
的指针,但是在runtime相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL只是一个保存方法名的字符串
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);
SEL sel1 = @selector(test);
NSLog(@"%s", sel1);
复制代码
输出为:
2021-05-10 21:58:24.705590+0800 RuntimeDemo[2266:67998] viewDidLoad
2021-05-10 21:58:24.705746+0800 RuntimeDemo[2266:67998] test
复制代码
- IMP _Nonnull method_imp 方法实现
IMP的定义同样在objc/objc.h
中
/// 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用来找到函数的地址,然后执行函数
3. char * _Nullable method_types; 方法类型
方法类型method_types是个字符串,用来存储方法的参数类型和返回值类型
到这里,
Method
的结构就已经很清楚了,Method
将SEL(方法名)
和IMP(函数指针)
关联起来,当对一个对象发送消息时,会通过给出的SEL(方法名)
去找到IMP(函数指针)
,然后执行
5. Runtime消息转发
在3. 消息机制的基本原理最后一步我们提到:若找不到对应的selector,消息被转发或者临时向receiver
添加这个selector对应的实现方法,否则就会崩溃
当一个方法找不到的时候,Runtime提供了消息动态解析、消息接受者重定向、消息定向等三步处理消息,具体流程如下
5.1 消息动态解析
Objective-C运行时会调用+resolveClassMethod
或+resolveInstanceMethod
,让你有机会提供一个函数实现。前者在对象方法未找到时调用,后者在类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES,那运行时系统就会重新启动一次消息发送的过程
主要用到的方法如下
动态解析的方法位于
// 位于objc/NSObject.h
+ (BOOL)resolveClassMethod:(SEL)sel ;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//位于 objc/runtime.h
/**
* 向一个类添加新方法,此方法需要给定名称及参数
*
* @param cls 要被添加方法的类
* @param name selector方法名称
* @param imp 实现方法的函数指针
* @param types 只想函数的返回值与参数类型
*
* @return 如果添加方法成功返回YES,否则返回NO
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) ;
复制代码
代码示例:
//
// ViewController.m
// RuntimeDemo
//
// Created by Terence on 2021/5/10.
// Copyright © 2021年 Terence. All rights reserved.
//
#import "ViewController.h"
#import "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(eat)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
class_addMethod(self.class, sel, (IMP)eatMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void eatMethod(id obj, SEL _cmd) {
NSLog(@"eat food");
}
@end
复制代码
输出结果:
2021-05-10 23:10:23.110858+0800 RuntimeDemo[3451:122697] eat food
复制代码
从上边的例子中,我们可以看出,虽然我们没有实现fun方法,但是通过重写resolveInstanceMethod
方法,利用class_addMethod
方法动态的添加了对象方法eatMethod
,并执行,成功调用了eatMethod
方法
class_addMethod方法中的特殊参数
v@:
,可参考苹果官方文档中关于Type Encodings
的说明:Type Encodings