总所周知,高级语言想要成为可执行文件须要 先编译为汇编语言 -> 再汇编为机器语言,机器语言也就是计算机可以识别的惟一语言,可是OC并不能直接编译为汇编语言,而是须要先转写为纯C语言再进行编译和汇编的操做。html
从OC到C语言的过渡就是由RunTime来实现的,然而OC是进行面向对象的开发,而C语言更多的是面向过程开发,这就须要将面向对象的类转变为面向过程的结构体。ios
RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。git
Objective-C语言做为一门动态语言,就意味着它不只须要一个编译器,也须要一个运行时系统来动态得建立类和对象、进行消息传递和转发等。这种动态语言的优点在于:咱们的代码更具备灵活性。而这个运行时系统就是Objc RunTime。github
Objc RunTime
实际上是一个RunTime库,它基本上是使用 C 和 汇编 语言写的,具备面向对象的能力,是Objective-C面向对象和动态机制的基石。api
对于C语言,函数的调用在编译的时候会决定调用哪一个函数数组
对于OC语言,是属于动态调用的,在编译时并不能决定真正调用哪一个函数,只有在真正运行的时候才会根据函数的名称找到对应函数来调用。缓存
想了解清RunTime的消息传递机制,首先咱们须要先对下面的一些内容有个概念性的认识。bash
SEL
又叫选择器,是表示一个方法的selector
的指针,其定义以下架构
typedef struct objc_selector *SEL;
复制代码
objc_selector
结构体的详细定义没有在<objc/runtime.h>
头文件中找到。方法的selector
用于表示运行时方法的名字。Objective-C在编译时,会依据每个方法的名字、参数序列,生成一个惟一的整型标识(Int类型的地址),这个标识就是SEL
。以下代码所示:app
SEL sel1 = @selector(testMethod1);
NSLog(@"sel1: %p", sel1);
复制代码
上面代码的输出为:
2019-10-31 00:33:23.271841+0800 RunTimeTestDemo[4736:725890] sel1: 0x103f2b856
复制代码
本质上,SEL
只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能惟一表明一个方法)。SEL
其主要做用是快速的经过方法名字查找到对应方法的函数指针,而后调用其函数。
工程中的全部的SEL
组成一个Set集合
,而Set
的特色就是惟一,所以SEL
也是惟一的。因此,若是咱们想到这个方法集合中查找某个方法时,只须要去找到这个方法对应的SEL
就行。
在运行时咱们能够添加新的selector
或者获取已知的selector
,有下面三种方法能够实现获取SEL
:
sel_registerName
函数@selector()
NSSelectorFromString()
方法IMP
其实是一个函数指针,指向方法实现的首地址。其定义以下:
id (*IMP)(id, SEL, ...)
复制代码
这个函数使用当前CPU架构实现的标准的C调用约定。
第一个参数是指向 self 的指针(若是是实例方法,则是类实例的内存地址;若是是类方法,则是指向元类的指针)
第二个参数是方法选择器 (selector
)
接下来是方法的实际参数列表。
前面介绍过的SEL
就是为了查找方法的最终实现IMP
的。因为每一个方法对应惟一的SEL
,所以咱们能够经过SEL
方便快速准确地得到它所对应的IMP
,查找过程将在下面讨论。
取得IMP
后,咱们就得到了执行这个方法代码的入口点,此时,咱们就能够像调用普通的C语言函数同样来使用这个函数指针了。
经过取得IMP
,咱们能够跳过 Runtime 的消息传递机制,直接执行IMP
指向的函数实现,这样省去了 Runtime 消息传递过程当中所作的一系列查找操做,会比直接向对象发送消息高效一些。
上面介绍完了SEL
和IMP
,咱们就能够来说讲Method
了。Method
用于表示类定义中的方法,其定义以下:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
复制代码
咱们能够看到该结构体中包含一个 SEL
和 IMP
,实际上至关于在 SEL
和 IMP
之间做了一个映射。有了SEL
,咱们即可以找到对应的IMP
,从而调用方法的实现代码。具体操做流程咱们将在下面讨论。
每个类都有一个方法列表Method List
,它保存着类里面全部的方法,根据SEL
传入的方法编号找到对应的方法,而后找到方法的实现,最后在方法的实现里面实现对应的具体操做。
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
复制代码
Objective-C 类是由Class
类型来表示的,它其实是一个指向objc_class
结构体的指针。它的定义以下:
typedef struct objc_class *Class;
复制代码
查看objc/runtime.h
中objc_class
结构体的定义以下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta 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 *` */
复制代码
在这个定义中,着重注意下面几个字段:
isa
:须要注意的是在 Objective-C 中,全部的类自身也是一个对象,这个对象的Class
里面也有一个isa
指针,它指向metaClass
(元类)。
meta-class
的方法列表中查找。super_class
:指向该类的父类,若是该类已是最顶层的根类(如NSObject或NSProxy),则super_class
为NULL。
cache
:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa
指针去查找可以响应这个消息的对象。在实际使用中,这个对象只有一部分方法是经常使用的,不少方法其实不多用或者根本用不上。这种状况下,若是每次消息来时,咱们都是methodLists
中遍历一遍,性能势必不好。这时,cache
就派上用场了。在咱们每次调用过一个方法后,这个方法就会被缓存到cache
列表中,下次调用的时候runtime就会优先去cache
中查找,若是cache
没有,才去methodLists
中查找方法。这样,对于那些常常用到的方法的调用,但提升了调用的效率。
version
:咱们可使用这个字段来提供类的版本信息。这对于对象的序列化很是有用,它但是让咱们识别出不一样类定义版本中实例变量布局的改变。
消息的关键在于objc_class
结构体,这个结构体有两个字段是咱们在分发消息的关注的:
isa
methodLists
。当咱们建立一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象能够访问类及类的继承体系。
消息机制是运行时里面最重要的机制,OC是动态语言,本质都是发送消息,每一个方法在运行时会被动态转化为消息发送,即:objc_msgSend(receiver, selector)
要想了解消息的转发咱们须要先明确消息是如何被动态的找到和发送的。
栗子:
BackView *backView = [[BackView alloc] init];
[backView changeBgColor];
//编译时底层转化
//objc对象的isa指针指向他的类对象,从而能够找到对象上的方法
//SEL:方法编号,根据方法编号就能够找到对应方法的实现。
[backView performSelector:@selector(changeBgColor)];
//performSelector本质即为运行时,发送消息,谁作事情就调用谁
objc_msgSend(backView, @selector(changeBgColor));
// 带参数
objc_msgSend(backView, @selector(changeBgColor:),[UIColor RedColor]);
复制代码
//本质是将类名转化成类对象,初始化方法实际上是建立类对象。
[BackView changeBgColor];
//BackView 只是表示一个类名,调用方法实际上是用的类对象去调用的。(类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向本身,superclass指针指向NSObject类。)
//编译时底层转化
//RunTime 调用类方法一样,类方法也是类对象去调用,因此须要获取类对象,而后使用类对象去调用方法
Class backViewClass = [BackView class];
[backViewClass performSelector:@selector(changeBgColor)];
//performSelector本质即为运行时,发送消息,谁作事情就调用谁
//类对象发送消息
objc_msgSend(backViewClass, @selector(changeBgColor));
// 带参数
objc_msgSend(backViewClass, @selector(changeBgColor:),[UIColor RedColor]);
复制代码
一个对象的方法像这样[obj changeBgColor]
,编译器转成消息发送objc_msgSend(obj, changeBgColor)
,Runtime 时执行的流程是这样的:
[objc performSelector:@selector(SEL)];
方法,编译器将代码转化为objc_msgSend(receiver, selector)
。objc_msgSend
函数中:
objc
的isa
指针找到objc
对应的class
类的结构体class
中,先去cache
中经过SEL
查找对应函数的 method
,若是找到则经过 method
中的函数指针跳转到对应的函数中去执行。cacha
中未找到,再去methodList
中查找,若是能找到,则将method
加入到cache
中,以方便下次查找,并经过method
中的函数指针跳转到对应的函数中去执行。methodlist
中未找到,则经过objc_msgSend
结构体中的指向父类的指针找到其父类,并在superClass
的分发表中去查找方法的selector
,若是能找到,则将method
加入到cache
中,以方便下次查找,并经过method
中的函数指针跳转到对应的函数中去执行。NSObject
类。selector
,则会走消息转发流程。咱们对消息的传递有了必定了解,当一个对象能接收一个消息时,就会走正常的方法调用流程。但若是一个对象没法接收指定消息时,又会发生什么事呢?
默认状况下,若是是以[object message]
的方式调用方法,若是object
没法响应message
消息时,编译器会报错。但若是是以perform...
的形式来调用,则须要等到运行时才能肯定object是否能接收message
消息。若是不能,则程序崩溃并抛出异常,经过控制台,咱们能够看到如下异常信:
- xxxx : unrecognized selector sent to instance xxxx
复制代码
这段异常信息其实是由 NSObject
的”doesNotRecognizeSelector
“方法抛出的。
为了不程序泵能够,咱们能够采起一些措施,让咱们的程序执行特定的逻辑,从而避免崩溃。这就启动了所谓的”消息转发(message forwarding)“机制,经过这一机制,咱们能够告诉对象如何处理未知的消息。
消息转发机制的三个步骤
对象在接收到未知的消息时,首先会调用所属类的实例方法 +resolveInstanceMethod:
或者类方法 +resolveClassMethod:
。
在这个方法中,咱们有机会为该未知消息新增一个”处理方法”。不过使用该方法的前提是咱们已经实现了该”处理方法”,只须要在运行时经过class_addMethod
函数动态添加到类里面就能够了。以下代码所示:
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
复制代码
在objc运行时会调用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数的实现。若是你添加了函数,那运行时系统就会从新启动一次消息发送的过程,不然 ,运行时就会移到下一步,消息转发(Message Forwarding)。
若是在上一步没法处理消息,则 Runtime 会继续调如下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
复制代码
若是一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会做为消息的新接收者,且消息会被分发到这个对象。固然这个对象不能是self
自身,不然就是出现无限循环。整个消息发送的过程会被重启,而且发送的对象会变成你返回的那个对象。固然,若是咱们没有指定相应的对象来处理 aSelector
,那么应该调用父类的实现来返回结果。
使用这个方法一般是在对象内部,可能还有一系列其它对象能处理该消息,咱们即可借这些对象来处理消息并返回,这样在对象外部看来,仍是由该对象亲自处理了这一消息。
这一步合适于咱们只想将消息转发到另外一个能处理该消息的对象上。但这一步没法对消息进行处理,如操做消息的参数和返回值等。
若是在上一步还不能处理未知消息,则惟一能作的就是启用完整的消息转发机制进行消息重定向了。这个时候 RunTime
会将未知消息的全部细节都封装为 NSInvocation
对象,而后调用下述方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
复制代码
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会建立一个表示消息的NSInvocation
对象,把 与还没有处理的消息有关的所有细节都封装在anInvocation
中,包括selector
,目标(target
)和参数。咱们能够在forwardInvocation
方法中选择将消息转发给其它对象。
forwardInvocation:
方法的实现有两个任务:
定位能够响应封装在anInvocation
中的消息的对象。这个对象不须要能处理全部未知消息。
使用anInvocation
做为参数,将消息发送到选中的对象。anInvocation
将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
还有一个很重要的问题,咱们必须重写如下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
复制代码
消息转发机制 须要使用从这个方法中获取的信息来建立NSInvocatio
n对象。所以咱们必须重写这个方法,为给定的selector
提供一个合适的方法签名。
从某种意义上来说,forwardInvocation:
就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也能够像一个运输站同样将全部未知消息都发送给同一个接收对象。这取决于具体的实现。
调用这个方法若是不能处理就会调用父类的相关方法,一直到NSObject
的这个方法,若是NSObject
都没法处理就会调用doesNotRecognizeSelector:
方法抛出异常。
NSTimer常见的使用方式
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
复制代码
主要是NSTimer的target被强引用了,而一般target就是所在的控制器,他又强引用的timer,形成了循环引用。下面是target参数的说明:
target: The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
复制代码
在这里首先声明一下:不是全部的NSTimer都会形成循环引用。就像不是全部的block都会形成循环引用同样。如下两种timer不会有循环引用:
非repeat类型的。非repeat类型的timer不会强引用target,所以不会出现循环引用。
block类型的,新api。iOS 10以后才支持,所以对于还要支持老版本的app来讲,这个API暂时没法使用。固然,block内部的循环引用也要避免。
NSTimer循环引用示例
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer * timer;
@property (nonatomic, assign) NSInteger number;
@property (weak, nonatomic) IBOutlet UILabel *timeLab;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
_number = 0;
}
- (void)dealloc {
NSLog(@"TimerViewController 界面销毁");
if (_timer) {
[_timer invalidate];
_timer = nil;
}
}
- (void)timerRun {
_number++;
NSLog(@"_number: %ld", _number);
_timeLab.text = [NSString stringWithFormat:@"定时时间:%ld", (long)_number];
}
@end
复制代码
咱们的初衷是想在界面销毁的时候释放timer,可是因为控制器与timer之间相互引用着,致使内存泄漏,没法释放。
//代码改动
// 将timer的类型变为了weak,其余不变
@property (nonatomic, weak) NSTimer *timer;
复制代码
经尝试,然并卵。
由于虽然这里没有循环引用了,可是RunLoop依旧引用着timer,而timer又引用着VC,虽然在pop的时候指向VC的强指针销毁了,可是仍然有timer的强指针指向VC,所以仍旧有内存泄漏。
//.h文件
@interface GYTimerProxy : NSObject
+ (instancetype) timerProxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
//.m文件
#import "GYTimerProxy.h"
@implementation GYTimerProxy
+ (instancetype) timerProxyWithTarget:(id)target {
GYTimerProxy *proxy = [[GYTimerProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
复制代码
VC控制器里只须要修改下面一句代码便可
//这里的target发生了变化
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[GYTimerProxy timerProxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
复制代码
咱们借助一个中间代理对象GYTimerProxy
,让VC控制器不直接持有timer
,而是持有GYTimerProxy
实例,让GYTimerProxy
实例来弱引用VC控制器,timer
强引用GYTimerProxy
实例。
实践尝试,颇有效果。
dealloc
方法,在dealloc
里调用了[self.timer invalidate]
,那么timer
将从RunLoop中移除,3号指针会被销毁。GYTimerProxy
也就自动销毁了。NSProxy是一个专门用于作消息转发的类,咱们须要经过子类的方式来使用它。
//.h文件
@interface GYProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
//.m文件
#import "GYProxy.h"
@implementation GYProxy
+ (instancetype)proxyWithTarget:(id)target {
GYProxy *proxy = [GYProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
复制代码
VC控制器里也只须要修改下面一句代码便可
//这里的target发生了变化
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[GYProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
复制代码
看上去方法二和方法三彷佛没有什么区别,但实际原理仍是略有不一样的:
GYTimerProxy
的父类是NSObject
,GYProxy
的父类是NSProxy
GYTimerProxy
只实现了forwardingTargetForSelector:
方法,可是GYProxy
是实现了methodSignatureForSelector:
和forwardInvocation:
。NSProxy具体是什么?
NSProxy
是一个专门用来作消息转发的类NSProxy
是个抽象类,使用需本身写一个子类继承自NSProxy
NSProxy
的子类须要实现两个方法,就是上面那两个OC中消息的转发
经过上面的RunTime咱们了解OC中消息转发的机制,当某个对象的方法找不到的时候,最后抛出doesNotRecognizeSelector:
的时候,会经历如下几个步骤:
1.消息发送,从方法缓存中找方法,找不到去方法列表中找,找到了将该方法加入方法缓存,仍是找不到,去父类里重复前面的步骤,若是找到底都找不到那么进入
2.动态方法解析,看该类是否实现了resolveInstanceMethod:
或resolveClassMethod:
,若是实现了就解析动态添加的方法,并调用该方法,若是没有实现进入
3.消息转发,这里分二步
forwardingTargetForSelector:
,看返回的对象是否为nil,若是不为nil,调用objc_msgSend
传入对象和SEL。methodSignatureForSelector:
返回方法签名,若是方法签名不为nil,调用forwardInvocation:
来执行该方法从上面能够看出,当继承自 NSObject
的对象,方法没有找到实现的时候,是须要通过第1步,第2步,第3步的操做才能抛出错误,若是在这个过程当中咱们作了补救措施,好比GYTimerProxy
就是在第3步的第1小步作了补救,那么就不会抛出doesNotRecognizeSelector:
,程序就能够正常执行。
可是若是是继承自 NSProxy
的 GYProxy
,就会跳过前面的全部步骤,直接到第3步的第2小步,直接找到对象,执行方法,提升了性能。
object_
开头class_
开头method_
开头ivar_
开头property_开头
开头protocol_
开头根据以上的函数的前缀 能够大体了解到层级关系。
对于以objc_
开头的方法,则是RunTime
最终的管家,能够获取内存中类的加载信息,类的列表,关联对象和关联属性等操做。
交换方法实现的需求场景:本身建立了一个功能性的方法,在项目中屡次被引用,当项目的需求发生改变时,要使用另外一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。
能够在类的分类中,再写一个新的方法(是符合新的需求的),而后交换两个方法的实现。这样,在不改变项目的代码,而只是增长了新的代码 的状况下,就完成了项目的改进。
交换两个方法的实现通常写在类的load方法里面,由于load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用屡次。
用到的方法名以下:
//获取方法地址
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//交换方法地址,至关于交换实现方式
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
复制代码
关联对象不是为类\对象添加属性或者成员变量(由于在设置关联后也没法经过ivarList或者propertyList取得) ,而是为类添加一个相关的对象,一般用于存储类信息,例如存储类的属性列表数组,为未来字典转模型的方便。
例如:给分类(通常系统类)添加属性
// 根据关联的key,获取关联的值。
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//将key跟关联的对象进行绑定
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
复制代码
开发使用场景:若是一个类方法很是多,加载类到内存的时候也比较耗费资源,须要给每一个方法生成映射表,可使用动态给某个类,添加方法解决。
KVC:把字典中全部值给模型的属性赋值。这个是要求字典中的Key,必需要在模型里能找到相应的值,若是找不到就会报错。
可是,在实际开发中,从字典中取值,不必定要所有取出来。所以,咱们能够经过重写KVC 中的 forUndefinedKey
这个方法,就不会进行报错处理。
另外,咱们能够经过runtime的方式去实现。咱们把KVC的原理倒过来,经过遍历模型的值,从字典中取值。
RunTime
的功能远比咱们想象的强大,这也是OC的动态特性的奇妙之处。了解运行时机制有助于咱们更好的去了解程序底层的实现,在实际的开发中也能更灵活的应用这些机制,去实现一些特殊的功能等。 在此仅抛砖引玉,但愿你们能有更多的探索,期待一块儿分享和探讨。