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

JoyGY· 2019-11-07
本文来自 juejin ,作者 JoyGY

引言

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

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

什么是RunTime

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

Objective-C语言作为一门动态语言,就意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发等。这种动态语言的优势在于:我们的代码更具有灵活性。而这个运行时系统就是Objc RunTime

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

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

    • 在编码阶段,如果C语言调用未实现的函数就会报错

  • 对于OC语言,是属于动态调用的,在编译时并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应函数来调用。

    • 在编译阶段,OC可以调用任何函数,即使用这个函数并未实现,只要声明过就不会报错。

    • 当调用A对象上的某个方法B时,如果A对象并没有实现这个方法,可以通过“ 消息转发 ”来解决,只要对B方法进行声明,则在编译时不会报错。

消息相关常用内容

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

SEL

SEL又叫选择器,是表示一个方法的selector的指针,其定义如下

typedef struct objc_selector *SEL;

objc_selector结构体的详细定义没有在<objc/runtime.h>头文件中找到。方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下代码所示:

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. 消息重定向

    image.png

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

    第一步:动态方法解析

    对象在接收到未知的消息时,首先会调用所属类的实例方法 +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将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

    还有一个很重要的问题,我们必须重写以下方法:

    + (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;

    消息转发机制 需要使用从这个方法中获取的信息来创建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之间相互引用着,导致内存泄漏,无法释放。

    image.png

    循环引用之解决方案

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

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


    经尝试,然并卵。

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

    image.png

    方案二:借助中间代理间接持有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也就自动销毁了。

    image.png

    方案三:继承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)

    类/对象的关联对象

    关联对象不是为类\u5bf9象添加属性或者成员变量(因为在设置关联后也无法通过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详解