Runtime入门总结
- 简介
- Runtime的基础数据结构
- 消息发送
1. 方法调用流程
2. 动态方法解析
3. 快速转发
4. 标准转发- API使用
1. 动态创建类,添加方法
2. 分类中动态绑定属性
3. 字典转模型
4. 方法交换(method swizzling)
简介
-
runtime是到底是个什么狼狗?
runtime就是一个由汇编语言和c语言编写的库,它实现了OC语言面向对象和动态语言的特性。多数情况下runtime库都是在幕后工作,但是它也提供了一些API给我们使用。你可以在官方文档查看这些API的使用,也可以在这里下载runtime的开源代码来研究它的具体实现 -
runtime是根据什么原理来实现的?
答案很简单,就是消息机制。OC中[receiver message]
并不是简单的函数调用,它会被编译器转化为[objc_msgSend(receiver, selector)]
,解释为向object发送一条message消息,程序运行时根据receiver(消息的接受者)和selector来确定执行具体的操作,而不是在编译时决定。 -
clang -rewrite-objc Myclass.m
可以查看转换后的代码
Runtime的基础数据结构
objc_msgSend()
函数是所有消息发送的必经之路,在我们详细了解消息发送流程之前,先从objc_msgSend()
函数入手了解一下Runtime中的数据结构。
objc_msgSend(<#id _Nullable self#>, <#SEL _Nonnull op, ...#>)
SEL
objc_msgSend
第二个参数为SEL类型,表示方法选择器,在objc.h文件中可以看到其定义:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
实际上它就是一个映射方法的分段字符串。用于指明调用哪个方法,可以理解为区分方法的ID。可以使用@selector()、sel_registerName()或NSSelectorFromString()来获取选择器。
IMP
typedef void (*IMP)(void /* id, SEL, ... */ );
它其实就是一个函数指针,指向具体的方法实现,在同一个对象中SEL
和IMP
是一一对应的。
id
objc_msgSend
的第一个参数,大家对它都不陌生,可以接收OC中任何类型的对象。
typedef struct objc_object *id;
本质上就是一个结构体指针,指向类实例。接着看objc_object
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
包含一个isa
指针,指向它所属的类。
Class
typedef struct objc_class *Class;
又是一个指针,指向objc_class
,定义在runtime.h
中
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
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
}
其中包括指向父类的指针super_class
、类的名字name
、实例变量的大小instance_size
、成员变量列表ivars
、方法列表methodLists
、缓存cache
和协议列表protocols
等。其中methodLists
就是一个链表,存储所有的实例方法(Method
)
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
} method_list[1];
method_types
存储着方法的参数类型和返回值累型。
cache
用来缓存常用的方法,以达到优化方法查找效率的目的。
struct objc_cache {
unsigned int mask; /* total = mask + 1 */
unsigned int occupied;
Method buckets[1];
};
最重要的我们发现Class里也有一个isa
指针。
实例对象的isa
指针指向实例对象所属的类,那么类的isa指针指向哪里呢?
我们先看个图
image.png
- 类本身也是一个对象,类对象所属的类称之为元类(
MetaClass
) - 每个对象都是一个类的实例,类中定义了实例方法列表。对象的isa指针指向它所属的类
- 每个类都是它所属元类的实例,在元类中定义了类方法列表,类的isa指针指向它的元类
- 每个类都有一个与之相关的元类,所有元类最终都指向根元类(
Root meta class
),根元类的isa指向自身。 - 其实根元类的父类就是
NSObject
,根元类就是NSObject
的元类
消息发送
方法调用流程
假如有一个Person类,有一个run的实例方法。实例化一个对象p,[p run]
是怎么执行的?
- 根据对象p的isa指针,找到所属的类
- 根据selector在类的
cache
缓存中寻找方法实现的地址,找到执行,没找到执行下一步 - 在类的
methodLists
方法列表中寻找,找到执行,没找到执行下一步 - 根据类的
super_class
指针,到父类的缓存中寻找,没找到执行下一步 - 在父类的方法列表中寻找,如果没找,继续向上找,一直到NSObject
- 如果最终没找到,则会调用
resolveInstanceMethod
或者resolveClassMethod
方法,让我们可以动态添加方法实现
动态方法解析
.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)run;
+ (void)eat:(NSString *)str;
@end
.m文件
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
// 当找不到实例方法实现时,调用此方法。在此方法中我们动态添加一个实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
class_addMethod([self class], sel, imp_implementationWithBlock(^(id self){
NSLog(@"run");
}), "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 找不到类方法时调用,在此动态添加一个类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(eat:)) {
class_addMethod(object_getClass(self), sel, (IMP)eat, "v@:@");
// 如果返回NO,则会进入消息转发
return YES;
}
return [super resolveClassMethod:sel];
}
// 函数实现,函数默认都有两个参数
void eat(id self, SEL _cmd, NSString *str) {
NSLog(@"eat %@",str);
}
@end
需要注意类方法要添加到元类中。
-
[NSObject class]
返回类本身 -
[object class]
返回对象isa所指的类,简单点说对象是哪个类的实例,就返回哪个类 -
object_getClass
返回传入对象的isa所指的类,如果传入的是一个实例,则返回实例所属的类;如果传入的是一个类,则返回类isa指针指向的类,也就是元类 -
v@:@
是描述函数的参数类型以及返回值类型的类型编码,v
代表函数的返回值为void,第一个@
代表第一个参数也就是id self
,:
代表第二个参数SEL _cmd
,第二个@
代表第三个参数NSString *str
。类型编码可参考类型编码
如果以上两个方法返回NO则会调用forwardingTargetForSelector
方法进入消息转发
快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
return [Proxy new];
}
return [super forwardingTargetForSelector:aSelector];
}
可以在此将消息转发给其他对象。所以我们需要一个可以相应此消息的对象。新建一个类Proxy
,只需在.m文件中给出方法实现。
#import "Proxy.h"
@implementation Proxy
- (void)run{
NSLog(@"proxy run");
}
@end
这样便完成了快速转发,当Person
实例调用run
方法时,就会转发到Proxy
中,如果Proxy
类里有对应的实现,则会执行。
在forwardingTargetForSelector
中,如果返回nil
或者self
则会进入标准转发forwardInvocation
。
标准转发
在调用forwardInvocation
之前,会先调用methodSignatureForSelector
获取方法签名,方法签名中包含了参数,返回值,以及消息接受者的相关信息。然后包装成一个NSInvocation
对象调用forwardInvocation
进行最后的消息转发。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
// 构造一个方法签名
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
return sig;
}
return [super methodSignatureForSelector:aSelector];
}
// 可以将消息转发,可以更改参数值,还可以更改所要调用的方法。总之可以肆无忌惮的做任何事情
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 将方法选择器更改为eat方法
[anInvocation setSelector:sel_registerName("eat:")];
NSString *str = @"apple";
// 添加参数,函数默认有两个参数,所以我们添加参数下标要从2开始
[anInvocation setArgument:&str atIndex:2];
// 转发给Proxy 对象
[anInvocation invokeWithTarget:[Proxy new]];
}
如果在消息标准转阶段不做处理,最后就会抛出unrecognized selector
异常,导致程序crash 。
总体来说消息发送的过程可以归纳成下图:
如果想更加深入了解请看 消息发送与转发机制原理
Runtime API的使用
动态创建类、添加方法、变量
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 动态创建一个类
Class DynaClass = objc_allocateClassPair([NSObject class], "DynaClass", 0);
// 从类中取出一个方法
Method des = class_getClassMethod([NSObject class], @selector(description));
// 获取方法的特征,包含参数与返回值的信息
const char* types = method_getTypeEncoding(des);
// 添加实例方法
class_addMethod(DynaClass, @selector(objcMethod), (IMP)objcMethod, types);
//添加一个成员变量,只能在objc_registerClassPair之前添加
class_addIvar(DynaClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
// 注册类
objc_registerClassPair(DynaClass);
// 获取元类
Class metaCls = objc_getMetaClass("DynaClass");
// 添加一个类方法
class_addMethod(metaCls, NSSelectorFromString(@"classMethod"), imp_implementationWithBlock(^(id self, NSString *str){
NSLog(@"class method %@", str);
}), "v@:");
// 根据动态创建的类,实例化一个对象
id dynaObjc = [[DynaClass alloc] init];
// 访问成员变量
[dynaObjc setValue:@"动态添加属性" forKey:@"name"];
NSLog(@"%@", [dynaObjc valueForKey:@"name"]);
// 调用方法
NSString *res = [dynaObjc performSelector:@selector(objcMethod)];
NSLog(@"%@", res);
[DynaClass performSelector:@selector(classMethod) withObject:@"我是参数"];
}
NSString *objcMethod(id self, SEL _cmd)
{
return [NSString stringWithFormat:@"hello"];
}
分类中动态绑定属性
分类(Category
)本来是不支持添加属性的,即使我们使用@property
也只会声明setter
、getter
,并没有生成对应的实例变量和方法实现。我们可以使用runtime
进行动态绑定来达到添加属性的效果,但是实质上只是添加一个关联,并不是真正的添加一个变量到类的地址空间中。
- (void)setTitle:(NSString *)title {
objc_setAssociatedObject(self, "title", title, OBJC_ASSOCIATION_COPY);
}
- (NSString *)title {
return objc_getAssociatedObject(self, "title");
}
详细请看关联对象实现原理
字典转模型
- 获取model对象的所有属性
- 根据属性名字查找字典中的key,取出对应的value
- 赋值给model
@implementation NSObject (Model)
+ (id)modelWithDic:(NSDictionary *)dic {
id pModel = [[self alloc] init];
unsigned int count;
// 获取对象的成员变量数组
Ivar *iList = class_copyIvarList(self, &count);
for (int i=0; i<count; i++) {
Ivar var = iList[i];
// 获取变量的名字
NSString *varName = [NSString stringWithUTF8String:ivar_getName(var)];
// 获取变量的类型
NSString *varType = [NSString stringWithUTF8String:ivar_getTypeEncoding(var)];
// 成员变量都是以下划线开头,所以需要截取一下
varName = [varName substringFromIndex:1];
varType = [varType substringWithRange:NSMakeRange(2, varType.length - 3)];
// 根据属性名获取字典的value
id value = dic[varName];
// 模型嵌套模型。字典的值是字典,需要将其也转换成对应模型
if ([value isKindOfClass:[NSDictionary class]] && ![varType hasPrefix:@"NS"]) {
Class class = NSClassFromString(varType);
value = [class modelWithDic:value];
}
// 字典的值是数组,数组包含字典,将数组中的字典也转成模型
if ([value isKindOfClass:[NSArray class]]) {
// 判断是否实现modelClassInArray协议,协议方法返回数组中字典对应的model类
if ([self respondsToSelector:@selector(modelClassInArray)]) {
id idSelf = self;
NSString *type = [idSelf modelClassInArray][varName];
Class class = NSClassFromString(type);
NSMutableArray *modelArray = [[NSMutableArray alloc] init];
for (NSDictionary *dic in value) {
id model = [class modelWithDic:dic];
[modelArray addObject:model];
}
value = modelArray;
}
}
if (value) {
[pModel setValue:value forKey:varName];
}
}
free(iList);
return pModel;
}
@end
MJExtension
、JSONModel
等大部分框架应该也是这种方式实现的。
方法交换
直白点就是调用A的时候,执行的是B的实现,调用B的时候,其实执行的是A。其实就是将两个方法的实现进行交换,如下图:
举个例子,在执行
[NSURL URLWithString:urlStr]
这句代码的时候,如果urlStr
包含中文,需要先对其进行编码才能正确返回NSURL对象。那么可不可以只修改某一个地方,不用每次调用URLWithString :
前都对urlStr
进行编码呢?
这时就可以利用方法交换来达到目的。
- 新建一个
NSURL
的分类 - 在分类添加一个方法
my_ URLWithString :
,进行处理中文问题 - 在
load
方法中将系统的URLWithString :
和我们新添的my_ URLWithString :
进行交换。
#import "NSURL+category.h"
@implementation NSURL (category)
+ (instancetype)my_URLWithString:(NSString *)URLString
{
// 此处不会造成死循环,因为my_URLWithString和URLWithString已经交换,
// 所以调用my_URLWithString实际上就是调用的URLWithString
return [self my_URLWithString:[URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
}
+ (void)load {
// 为了确保只执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getClassMethod(self, @selector(URLWithString:));
Method swizzledMethod = class_getClassMethod(self, @selector(my_URLWithString:));
/*
class_addMethod:如果类中存在方法,则添加失败。如果不存在则添加成功
判断是否添加成功的目的:
如果本类中没有实现originalMethod,但是父类中实现了。
直接使用method_exchangeImplementations进行交换,
交换的两个方法就是父类中的originalMethod和swizzledMethod。
那么父类的其他子类调用originalMethod也会执行swizzledMethod。
进行判断就是为了避免这种情况以及带来的其他麻烦
在本分类中,因为确定URLWithString一定实现了,可以直接使用method_exchangeImplementations进行交换
*/
BOOL didAddMethod = class_addMethod(object_getClass(self), @selector(URLWithString:), method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// class_replaceMethod:替换方法的实现
class_replaceMethod(self, @selector(my_URLWithString:), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else {
// 将两个方法交换
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
@end
这样直接使用[NSURL URLWithString:urlStr]
就可以了,不必再去理会urlStr
中是否存在中文。
参考:
iOS 模块分解—「Runtime面试、工作」看我就 🐒 了 _.
Objective-C Method Swizzling
Objective-C Runtime
iOS runtime和runloop