Runtime
概念:
Runtime是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。Runtime的最大特征就是实现了OC语言的动态特性。
消息机制原理
在Object-C的语言中,对象方法调用都是类似[receiver selector]
的形式,其本质:就是让对象在运行时发送消息的过程。
而方法调用[receiver selector]
分为两个过程:
- 编译阶段
[receiver selector]
方法被编译器转化,分为两种情况:
- 不带参数的方法被编译为:objc_msgSend(receiver,selector)
- 带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)
- 运行时阶段
消息接收者recever
寻找对应的selector
,也分为两种情况:
- 接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
- 接收者找不到对应的selector,消息被转发或者临时向接收者添加这个selector对应的实现内容,否则崩溃
总而言之:
OC调用方法[receiver selector],编译阶段确定了要向哪个接收者发送message消息,但是接收者如何响应决定于运行时的判断。
重要概念
objc_msgSend
所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector); 是 [receiver selector]; 对应的 C 函数。
Object(对象)
在 objc/runtime.h 中
,Object(对象)
被定义为指向 objc_object
结构体 的指针,objc_object
结构体 的数据结构如下:
//runtime对objc_object结构体的定义
struct objc_object {Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};//id是一个指向objc_object结构体的指针,即在Runtime中:
typedef struct objc_object *id;//OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa(指向对象的类)的指针的
Class(类)
在 objc/runtime.h 中
,Class(类)
被定义为指向 objc_class
结构体 的指针,objc_class
结构体 的数据结构如下:
//runtime对objc_class结构体的定义
struct objc_class {Class _Nonnull isa; // objc_class 结构体的实例指针#if !__OBJC2__Class _Nullable super_class; // 指向父类的指针const char * _Nonnull name; // 类的名字long version; // 类的版本信息,默认为 0long 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};//class是一个指向objc_class结构体的指针,即在Runtime中:
typedef struct objc_class *Class;
SEL (方法选择器)
typedef struct objc_selector *SEL;//Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
1.不同类中相同名字的方法对应的方法选择器是相同的。
2.即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。
获取SEL有三种方法:
1.OC中,使用@selector(“方法名字符串”)
2.OC中,使用NSSelectorFromString(“方法名字符串”)
3.Runtime方法,使用sel_registerName(“方法名字符串”)
Method(方法)
在 objc/runtime.h 中
,Method(方法)
被定义为指向 objc_method
结构体 的指针,在objct_class
定义中看到methodLists
,其中的元素就是Method
,objc_method
结构体 的数据结构如下:
struct objc_method {SEL _Nonnull method_name; // 方法名char * _Nullable method_types; // 方法类型IMP _Nonnull method_imp; // 方法实现
};//Method表示某个方法的类型
typedef struct objc_method *Method;
Runtime消息转发
动态方法解析:动态添加方法
Runtime足够强大,能够在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:
1. 动态添加未实现方法,解决代码中因为方法未找到而报错的问题
2. 利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法
方法动态解析主要用到的方法如下:
//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel//Runtime方法:
/**运行时方法:向指定类中添加特定方法实现的操作@param cls 被添加方法的类@param name selector方法名@param imp 指向实现方法的函数指针@param types imp函数实现的返回值与参数类型@return 添加方法是否成功*/
BOOL class_addMethod(Class _Nullable cls,SEL _Nonnull name,IMP _Nonnull imp,const char * _Nullable types)
- 解决方法无响应崩溃问题
执行OC方法其实就是一个发送消息的过程,若方法未实现,可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:
在这个过程中,可能还会使用到的方法有:
例子:
#import "ViewController.h"
#import <objc/runtime.h>@interface ViewController ()
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 fun 函数[self performSelector:@selector(fun)];
}// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMPclass_addMethod([self class], sel, (IMP)funMethod, "v@:");return YES;}return [super resolveInstanceMethod:sel];
}void funMethod(id obj, SEL _cmd) {NSLog(@"funMethod"); //新的 fun 函数
}
@end//日志输出:2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod
从执行任务的输出日志中,可以看到:
虽然没有实现 fun 方法,但是通过重写 resolveInstanceMethod: ,利用 class_addMethod 方法添加对象方法实现 funMethod 方法,并执行。从打印结果来看,成功调起了funMethod 方法。
消息接收者重定向
如果上一步中 +resolveInstanceMethod:
或者 +resolveClassMethod
: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。
如果当前对象实现了 -forwardingTargetForSelector:
,Runtime
就会调用这个方法,允许将消息的接受者转发给其他对象,其主要方法如下:
//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
例子:
#import "ViewController.h"
#import <objc/runtime.h>@interface Person : NSObject
- (void)fun;
@end@implementation Person- (void)fun {NSLog(@"fun");
}
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 fun 方法[self performSelector:@selector(fun)];
}+ (BOOL)resolveInstanceMethod:(SEL)sel {return YES; // 为了进行下一步 消息接受者重定向
}// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(fun)) {return [[Person alloc] init];// 返回 Person 对象,让 Person 对象接收这个消息}return [super forwardingTargetForSelector:aSelector];
}//日志输出:2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun
从执行任务的输出日志中,可以看到:
虽然当前 ViewController 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。
但是我们通过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了 Person 对象去执行了。
通过forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil
,也不是 self
,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程
消息重定向
如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector:
方法获取函数的参数和返回值类型。
其过程:
- 如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,
并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP(指向实现方法的函数指针) 的机会。 - 如果 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了。
所以可以在-forwardInvocation:
方法中对消息进行转发。
其主要方法:
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
例子:
#import "ViewController.h"
#import <objc/runtime.h>@interface Person : NSObject
- (void)fun;
@end@implementation Person
- (void)fun {NSLog(@"fun");
}
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 fun 函数[self performSelector:@selector(fun)];
}+ (BOOL)resolveInstanceMethod:(SEL)sel {return YES; // 为了进行下一步 消息接受者重定向
}- (id)forwardingTargetForSelector:(SEL)aSelector {return nil; // 为了进行下一步 消息重定向
}// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {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//日志输出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun
从执行任务的输出日志中,可以看到:
在 -forwardInvocation: 方法里面让 Person 对象去执行了 fun 函数
问:既然 -forwardingTargetForSelector:
和 -forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?
答:区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。
Runtime的应用
动态方法交换
实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:
通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能。
类目添加新的属性
在日常开发过程中,常常会使用类目Category
为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。
获取类详细属性
- 获取属性列表
- 获取所有成员变量
- 获取所有方法
- 获取当前遵循的所有协议
解决同一方法高频率调用的效率问题
Runtime源码中的IMP
作为函数指针,指向方法的实现。通过它,可以绕开发送消息的过程来提高函数调用的效率。当需要持续大量重复调用某个方法的时候,会十分有用。
动态操作属性
- 修改私有属性
- 改进iOS归档和解档
- 实现字典与模型的转换
利用Runtime
实现的思路大体如下:
借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。
Swift中的Runtime
Swift是静态语言,本身没有动态特性。
结论:
- 对于纯Swift类来说,没有动态特性。方法和属性不加任何修饰符的情况下,这个时候已经不具备我们所谓的Runtime特性了。
- 对于纯Swift类,方法和属性添加@objc标识的情况下,当前我们可以通过Runtime API拿到,但是在我们的OC中是没办法进行调度的。
- 对于继承自NSObject类来说,如果我们想要动态的获取当前的属性和方法,必须在其声明前添加@objc关键字,方法交换需要添加 dynamic 标识,否则也是无法通过Runtime API获取的。
反射
反射是Swift中动态获取的一种方法,可以动态获取类型、成员信息,在运行时可以调用方法、属性等行为的特性。上面的结论说了对于一个纯Swift类来说,并不支持像OC那样操作,但是Swift标准库依然提供了反射机制让我们访问成员信息。
用法如下:
import UIKit//下方OC的部分可以不加没问题
class LGTeacher: NSObject{@objc var age: Int = 18@objc dynamic func teach(){print("teach")}
}let t = LGTeacher()let mirror = Mirror(reflecting: t.self)
for pro in mirror.children{print("\(pro.label):\(pro.value)")
}
运行结果:
Optional("age"):18