一.RumTime是什么
Runtime是OC的重要特性,对于 C 语言,函数的调用会在编译期就已经决定好,在编译完成后直接顺序执行。但是 OC 是一门动态语言,函数调用变成了消息发送,在编译期不能知道要调用哪个函数。所以 Runtime 就是去解决如何在运行时期找到调用方法这样的技术。
nil,NSNULL,NULL的定义及区别
NULL在计算机世界中表示程序不执行/对象不存在/指针为空的一个概念,它是一个C/C++中的基本类型。
#define NULL ((void*)0)
nil是Objective-C中对于NULL的描述,同样表示不存在/不执行的概念,它是一个OC对象。
#ifndef nil
# if __has_feature(cxx_nullptr)
# define nil nullptr
# else
# define nil __DARWIN_NULL
# endif
#endif
#define nil ((void*)0)
NSNULL是Objective-C中对空值对象的表示,它的作用相当于一个占位对象。常用于在 Foundation 集合对象(NSArray、NSDictionary、NSSet)储存空值对象。
#import <Foundation/NSObject.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSNull : NSObject <NSCopying, NSSecureCoding>
+ (NSNull *)null;
@end
NS_ASSUME_NONNULL_END
在OC中对象发送消息的实现思路
Obejective-C是C语言的超集,通过它的运行机制直接映射到底层可以发现它的object和class都是一组C的结构体。
1.在OC中一个实例对象的本质就是一个指向Class的isa指针。
typedef struct objc_object {
Class isa;
} *id;
2.Objective-C所实现的Class内部的组成又分为两部分:这个Class的isa指针和父类指针。
struct objc_class {
Class isa; //------>meta-class(用于存储类方法的objc_class对象)
Class super_class; //------>父类对象指针
const charchar *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;//----->存储着该实例对象的方法列表
struct objc_cache *cache;
struct objc_protocol_list *protocols;
} OBJC2_UNAVAILABLE;
3.object中的isa指向objc_class中的isa,objc_class中的isa指向meta-class。
- 当我们对一个实例发送消息时(-开头的方法),会在该 instance 对应的类的 methodLists 里查找。
- 当我们对一个类发送消息时(+开头的方法),会在该类的 meta-class的 methodLists 里查找。
4.类中存放着实例方法列表(methodLists),在这个方法列表中 SEL 作为 key,IMP 作为 value。
在编译时期,根据方法名字会生成一个唯一的 Int 标识,这个标识就是 SEL。
IMP 其实就是函数指针,指向了最终的函数实现。
SEL 可以将其理解为方法的Key,其结构如下:
typedef struct objc_selector *SEL;
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;
char *types; OBJC2_UNAVAILABLE;
};
IMP 可以理解为方法的Value,具体为函数指针指向了最终的实现。
typedef id (*IMP)(id, SEL, ...);
OC 中不支持函数重载的原因就是因为一个类的方法列表中不能存在两个相同的 SEL 。但是多个方法却可以在不同的类中有一个相同的 SEL,不同类的实例对象执行相同的 SEL 时,会在各自的方法列表中去根据 SEL 去寻找自己对应的IMP。这使得OC可以支持函数重写。(不能重载但是能重写)
整个 Runtime 的核心就是 objc_msgSend 函数,通过给类发送 SEL 以传递消息,找到匹配的 IMP 再获取最终的实现。
二.RunTime如何实现
原文描述:https://www.jianshu.com/p/b8e2ca18cdcf
1. 类层次搜索
第一步 - 编译器转化
通过终端命令clang -rewrite-objc xxx.m可以看到xxx.m编译后的xxx.cpp
(C++文件),比对.m文件和.cpp文件,你会发现方括号形式的方法调用基于返回类型的不同被编译器转化成(objc_msgSend系列函数中的某一个)的调用。通过终端命令clang -rewrite-objc xxx.m可以看到xxx.m编译后的xxx.cpp
(C++文件),比对.m文件和.cpp文件,你会发现方括号形式的方法调用基于返回类型的不同被编译器转化成(objc_msgSend系列函数中的某一个)的调用。
// OC形式:
[receiver messageWithArgs:arg1 and:arg2 …];
// C语言函数及参数说明:
// ◈ receiver => 消息接收者,类型为id,通过其isa指针找到指定类的结构
// ◈ selector => 方法选择器,类型为SEL,用于在类结构的方法分发表中搜索指定名字的方法实现/地址
objc_msgSend(receiver, selector, arg1, arg2, ...)
隐藏参数
在方法的实现中(OC代码的花括号内)有两个隐藏参数可用:self
(receiver)和_cmd
(selector)
注意:
1、在实例方法中,self表示对象;在类方法中,self表示元类对象(即类)。
2、super关键字实际上会被转化成一个objc_super类型的结构体,其值为{self, self.superclass}。
struct objc_super {
id receiver;
Class class;
};
这也意味着在子类都没有重写class方法时,[self class];和[super class];最终调用的都是NSObject的class方法实现,而接收者都是self,所以两者返回都的都是self.class;要获取超类,正确的方法是使用[self superclass];
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
均打印son
第二步 - 追踪继承体系
通过objc_object/objc_class的isa
指针,沿着继承体系在每一个objc_class结构体中:
1、在cache
中查找指定SEL的实现,失败转2
2、在objc_method_list
中查找指定SEL的实现
获取方法的地址
如果同一个方法实现你需要调用一万次,那么通过NSObject的-methodForSelector:方法绕过动态绑定直接获取方法的实现会提高性能(1万次[a msg] =>一次-methodForSelector:+1万次IMP(a, msg, …)),因为减少了方法实现的搜索次数。
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
setter(target, @selector(setFilled:), YES);
2.消息转发
如果一直到root class都没有定位到SEL的实现,那么转入消息转发过程。
步骤一:动态解析
通过实现resolveInstanceMethod:/resolveClassMethod:方法,我们有机会为该未知消息(SEL)新增一个“处理方法”(IMP)。
- 这意味着在消息转发前,你有机会通过class_addMethod给类动态添加一些方法
- 实际上返回值YES/NO无关紧要,只要你在resovle过程中新增过方法,就会触发
class_getMethodImplementation
,其作用相当于重新启动一次消息发送过程。
步骤二:备用接收者
通过实现-forwardingTargetForSelector:方法将消息(SEL)直接转发给另一个对象(备用接收者),也就是在另一个对象(不能是nil或self)上重启消息发送过程。
步骤三:完整转发
1.通过实现-methodSignatureForSelector:提供方法签名(即参数和返回值的类型信息)
- 可通过调用其他类的
+instanceMethodSignatureForSelector:
方法或其他对象的-methodSignatureForSelector:
方法提供 - 也可通过+signatureWithObjCTypes:自行生成
2.生成的签名将和原始消息一起打包到一个NSInvocation对象中。
- 通过操作NSInvocation对象的target、selector属性可以方便地转发,甚至转发给另一个对象的另一个需要不同参数的SEL也是可以的。
- 通过-getArgument:atIndex:和-setArgument:atIndex:可以操作方法调用传入的参数。
- 通过-getReturnValue:和-setReturnValue:可以直接操作方法invoke后的返回值。
3.实现-forwardInvocation:方法
- 通过调用-invoke 方法重新启动一个消息发送过程。
- 不调用invoke,吞掉这个消息(不做任何处理)
3.消息转发的作用
转发和多继承转发和多继承
转发模拟了继承,所以可以用来为Objc程序提供类似多继承的功能。转发和多继承的区别如下:
- 多继承是将许多功能combine到一个对象中;
- 转发则将功能分解到多个对象,并一种对消息发送者透明的方式将它们关联起来;
代理/替代对象
场景描述:当你有一个对象,这个对象的设置由于需要处理大量数据非常耗时,所以更倾向于懒加载——在真正需要或系统空闲的时候来进行加载,这时你需要一个占位对象来使得应用的其他部分正常工作,这个占位对象的工作如下:
- 获取关于待加载数据的描述信息
- 转发消息时检测对象是否创建并已加载完数据,据此决定创建对象、丢弃消息或转发消息。
转发和继承
以下方法只考虑类继承体系(不含转发链);如需要对象表现得和继承一样,重写它们并把转发算法包括进来:
- -respondsToSelector: &+instancesRespondToSelector:
- -isKindOfClass: & -isMemberOfClass:
- -conformsToProtocol:
总结:
- 消息机制:继承体系搜索 ->消息转发 (动态解析->快速转发 ->完整转发)
- 转发和继承(-respondsToSelector:等)、多继承、代理。
三.Runtime的应用--KVO与KVC
KVC(Key-Value-Coding)和KVO(Key-Value-Observer)这两种技术均基于Runtime实现的。
KVC:键值编码
它能够通过类成员变量的名称字符串来访问这个成员变量的值。对于支持KVC的类,我们可以使用以下四个方法访问属性:
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
KVC的对象可以使用@来简化不可变对象的访问如:@"hello",@[@1,@"hello"]。
KVC的操作符
简单的集合操作符:返回值是NSString、NSNumber或者NSDate。
- @count:返回一个值为集合中对象总数的NSNumber对象。
- @sum: 首先把集合中的每个对象都转换为double类型,然后计算其总,最后返回一个值为这个总和的NSNumber对象。
- @avg: 首先把集合中的每个对象都转换为double类型,然后计算其平均值,最后返回一个值为这个总和的NSNumber对象。
- @max: 使用compare:方法来确定最大值。所以为了让其正常工作,集合中所有的对象都必须支持和另一个对象的比较。
- @min: 和@max一样,但是返回的是集合中的最小值。
products是数组,数组中存放了很多对象,每个对象都有一个price的属性。
[products valueForKeyPath:@"@sum.price"];
[@[@1,@2] valueForKey:@"@max.self"];
对象操算符:返回值是一个数组。
- @distinctUnionOfObjects:获取数组中每个对象的属性的值,放到一个数组中并返回,会对数组去重。
- @unionOfObjects: 同@distinctUnionOfObjects,但是不去重。
Person *lilei = [[Person alloc] init];
lilei.name = @"LiLei";
Person *hanMeiMei = [[Person alloc] init];
hanMeiMei.name = @"hanMeiMei";
NSArray *array = @[lilei, hanMeiMei];
NSLog(@"array is %@",[array valueForKeyPath:@"@distinctUnionOfObjects.name"]);
输出结果为:
array is (LiLei,hanMeiMei)
数组和集合操作符:返回值是一个数组或者集合。
- @distinctUnionOfArrays: 获取数组中每个数组中的每个对象的属性的值,放到一个数组中并返回,会对数组去重复。
- @unionOfArrays:同@distinctUnionOfArrays,但是不去重。
- @distinctUnionOfSets: 获取集合中每个集合中的每个对象的属性的值,放到一个集合中并返回。
Person *lilei = [[Person alloc] init];
lilei.name = @"LiLei";
Person *hanMeiMei = [[Person alloc] init];
hanMeiMei.name = @"hanMeiMei";
NSArray *array = @[lilei, hanMeiMei];
NSLog(@"array is %@",[ @[array,array] valueForKeyPath:@"@unionOfArrays.name"]);
输出结果为:
array is (LiLei,hanMeiMei,LiLei,hanMeiMei)
KVO:键值观察
它是基于KVC实现的一种观察者模式。
@interface BankAccount : NSObject
@property (nonatomic, assign) NSInteger balance;
@end
@interface Person : NSObject
@property (nonatomic, strong) BankAccount *account;
@end
@implementation Person
- (instancetype)init {
...
// 注册Observer:
[self.account addObserver:self
forKeyPath:@"balance"
options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld
context:nil];
...
}
- (void)dealloc {
// 不要忘了removeObserver
[self.account removeObserver:self forKeyPath:@"balance"];
}
// 属性变化的回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"balance"]) {
NSLog(@"Balance was %@.", change[NSKeyValueChangeOldKey]);
NSLog(@"Balance is %@ now.", change[NSKeyValueChangeNewKey]);
}
}
@end
- (void)testKVO {
Person *liSi = [[Person alloc] initWithName:@"liSi" andBalance:20];
// 无论是用点语法还是KVC的方法都会触发回调:
liSi.account.balance = 150;
[liSi setValue:@250 forKeyPath:@"account.balance"];
}