RunTime的消息机制 & NSTimer的循环引用

引言

总所周知,高级语言想要成为可执行文件须要 先编译为汇编语言 -> 再汇编为机器语言,机器语言也就是计算机可以识别的惟一语言,可是OC并不能直接编译为汇编语言,而是须要先转写为纯C语言再进行编译和汇编的操做。html

从OC到C语言的过渡就是由RunTime来实现的,然而OC是进行面向对象的开发,而C语言更多的是面向过程开发,这就须要将面向对象的类转变为面向过程的结构体。ios

什么是RunTime

RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。git

Objective-C语言做为一门动态语言,就意味着它不只须要一个编译器,也须要一个运行时系统来动态得建立类和对象、进行消息传递和转发等。这种动态语言的优点在于:咱们的代码更具备灵活性。而这个运行时系统就是Objc RunTimegithub

  • Objc RunTime 实际上是一个RunTime库,它基本上是使用 C汇编 语言写的,具备面向对象的能力,是Objective-C面向对象和动态机制的基石。api

  • 对于C语言,函数的调用在编译的时候会决定调用哪一个函数数组

    • 在编码阶段,若是C语言调用未实现的函数就会报错
  • 对于OC语言,是属于动态调用的,在编译时并不能决定真正调用哪一个函数,只有在真正运行的时候才会根据函数的名称找到对应函数来调用。缓存

    • 在编译阶段,OC能够调用任何函数,即便用这个函数并未实现,只要声明过就不会报错。
    • 当调用A对象上的某个方法B时,若是A对象并无实现这个方法,能够经过“ 消息转发 ”来解决,只要对B方法进行声明,则在编译时不会报错。

消息相关经常使用内容

想了解清RunTime的消息传递机制,首先咱们须要先对下面的一些内容有个概念性的认识。bash

SEL

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函数
  • Objective-C编译器提供的@selector()
  • NSSelectorFromString()方法

IMP

IMP其实是一个函数指针,指向方法实现的首地址。其定义以下:

id (*IMP)(id, SEL, ...)
复制代码

这个函数使用当前CPU架构实现的标准的C调用约定。

  • 第一个参数是指向 self 的指针(若是是实例方法,则是类实例的内存地址;若是是类方法,则是指向元类的指针)

  • 第二个参数是方法选择器 (selector)

  • 接下来是方法的实际参数列表。

前面介绍过的SEL就是为了查找方法的最终实现IMP的。因为每一个方法对应惟一的SEL,所以咱们能够经过SEL方便快速准确地得到它所对应的IMP,查找过程将在下面讨论。

取得IMP后,咱们就得到了执行这个方法代码的入口点,此时,咱们就能够像调用普通的C语言函数同样来使用这个函数指针了。

经过取得IMP,咱们能够跳过 Runtime 的消息传递机制,直接执行IMP指向的函数实现,这样省去了 Runtime 消息传递过程当中所作的一系列查找操做,会比直接向对象发送消息高效一些。

Method

上面介绍完了SELIMP,咱们就能够来说讲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;	// 方法实现
}
复制代码

咱们能够看到该结构体中包含一个 SELIMP ,实际上至关于在 SELIMP 之间做了一个映射。有了SEL,咱们即可以找到对应的IMP,从而调用方法的实现代码。具体操做流程咱们将在下面讨论。

Method List

每个类都有一个方法列表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;
}   
复制代码

Class

Objective-C 类是由Class类型来表示的,它其实是一个指向objc_class结构体的指针。它的定义以下:

typedef struct objc_class *Class;
复制代码

查看objc/runtime.hobjc_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 *` */ 
复制代码

在这个定义中,着重注意下面几个字段:

  1. isa:须要注意的是在 Objective-C 中,全部的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)。

    • 当咱们向一个对象发送消息时,RunTime 会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
  2. super_class:指向该类的父类,若是该类已是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。

  3. cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找可以响应这个消息的对象。在实际使用中,这个对象只有一部分方法是经常使用的,不少方法其实不多用或者根本用不上。这种状况下,若是每次消息来时,咱们都是methodLists中遍历一遍,性能势必不好。这时,cache就派上用场了。在咱们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,若是cache没有,才去methodLists中查找方法。这样,对于那些常常用到的方法的调用,但提升了调用的效率。

  4. version:咱们可使用这个字段来提供类的版本信息。这对于对象的序列化很是有用,它但是让咱们识别出不一样类定义版本中实例变量布局的改变。

消息的关键在于objc_class结构体,这个结构体有两个字段是咱们在分发消息的关注的:

  1. 指向父类的指针isa
  2. 一个类的方法分发表,即methodLists

当咱们建立一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象能够访问类及类的继承体系。

消息传递 - 动态查找

消息机制是运行时里面最重要的机制,OC是动态语言,本质都是发送消息,每一个方法在运行时会被动态转化为消息发送,即:objc_msgSend(receiver, selector)

要想了解消息的转发咱们须要先明确消息是如何被动态的找到和发送的。

栗子:

  • OC代码 - 实例方法 调用底层的实现:
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]);
复制代码
  • OC代码 - 类方法 调用底层的实现
//本质是将类名转化成类对象,初始化方法实际上是建立类对象。
[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函数中:
    • 首先经过objcisa指针找到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)“机制,经过这一机制,咱们能够告诉对象如何处理未知的消息。

消息转发机制的三个步骤

  1. 动态方法解析
  2. 备援接收者
  3. 消息重定向

下面咱们详细讨论一下这三个步骤。

第一步:动态方法解析

对象在接收到未知的消息时,首先会调用所属类的实例方法 +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
复制代码

消息转发机制 须要使用从这个方法中获取的信息来建立NSInvocation对象。所以咱们必须重写这个方法,为给定的selector提供一个合适的方法签名。

从某种意义上来说,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也能够像一个运输站同样将全部未知消息都发送给同一个接收对象。这取决于具体的实现。

调用这个方法若是不能处理就会调用父类的相关方法,一直到NSObject的这个方法,若是NSObject都没法处理就会调用doesNotRecognizeSelector:方法抛出异常。

NSTimer的循环引用

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;
复制代码
  • timerWith的方式建立,须要本身手动添加到runloop中执行,而且须要启动子线程的runloop。
  • scheduledTimerWith的方式建立,系统默认帮你添加到runloop的defaultmood中了。

NSTimer形成循环引用的缘由

主要是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之间相互引用着,致使内存泄漏,没法释放。

NSTimer引用图1

循环引用之解决方案

方案一:将timer的引用变为弱指针(❌)

//代码改动
// 将timer的类型变为了weak,其余不变
@property (nonatomic, weak) NSTimer *timer;
复制代码

经尝试,然并卵。

由于虽然这里没有循环引用了,可是RunLoop依旧引用着timer,而timer又引用着VC,虽然在pop的时候指向VC的强指针销毁了,可是仍然有timer的强指针指向VC,所以仍旧有内存泄漏。

方案二:借助中间代理间接持有timer(✅)

//.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实例。

实践尝试,颇有效果。

  • 当pop的时候,1号指针被销毁,VC控制器无强引用,能够被正常销毁
  • VC控制销毁,会走dealloc方法,在dealloc里调用了[self.timer invalidate],那么timer将从RunLoop中移除,3号指针会被销毁。
  • 当VC销毁了,2号指针天然也被销毁了
  • 此时timer已经没有被别的对象强引用了,因此timer会被销毁,代理实例GYTimerProxy也就自动销毁了。

方案三:继承NSProxy类对消息处理(✅)

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的父类是NSObjectGYProxy的父类是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。
    • 若是上面为nil,那么就调用methodSignatureForSelector:返回方法签名,若是方法签名不为nil,调用forwardInvocation:来执行该方法

从上面能够看出,当继承自 NSObject 的对象,方法没有找到实现的时候,是须要通过第1步,第2步,第3步的操做才能抛出错误,若是在这个过程当中咱们作了补救措施,好比GYTimerProxy就是在第3步的第1小步作了补救,那么就不会抛出doesNotRecognizeSelector:,程序就能够正常执行。

可是若是是继承自 NSProxyGYProxy,就会跳过前面的全部步骤,直接到第3步的第2小步,直接找到对象,执行方法,提升了性能。

Objc RunTime函数的定义

  • 对对象进行操做的方法通常以object_开头
  • 对类进行操做的方法通常以class_开头
  • 对类或对象的方法进行操做的方法通常以method_开头
  • 对成员变量进行操做的方法通常以ivar_开头
  • 对属性进行操做的方法通常以property_开头开头
  • 对协议进行操做的方法通常以protocol_开头

根据以上的函数的前缀 能够大体了解到层级关系。

对于以objc_开头的方法,则是RunTime最终的管家,能够获取内存中类的加载信息,类的列表,关联对象和关联属性等操做。

扩展 - 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实现

KVC:把字典中全部值给模型的属性赋值。这个是要求字典中的Key,必需要在模型里能找到相应的值,若是找不到就会报错。

可是,在实际开发中,从字典中取值,不必定要所有取出来。所以,咱们能够经过重写KVC 中的 forUndefinedKey这个方法,就不会进行报错处理。

另外,咱们能够经过runtime的方式去实现。咱们把KVC的原理倒过来,经过遍历模型的值,从字典中取值。

  • (RunTime的类和对象以及属性和成员变量的应用)

总结

RunTime 的功能远比咱们想象的强大,这也是OC的动态特性的奇妙之处。了解运行时机制有助于咱们更好的去了解程序底层的实现,在实际的开发中也能更灵活的应用这些机制,去实现一些特殊的功能等。 在此仅抛砖引玉,但愿你们能有更多的探索,期待一块儿分享和探讨。

参考文章:

iOS Runtime原理及使用

ios RunTime机制详解

Objective-C Runtime运行时

iOS runtime探究

iOS Runtime详解

相关文章
相关标签/搜索