简述runtime

作者: Miss_QL | 来源:发表于2018-03-13 11:38 被阅读19次

    1、runtime概述
    OC在三种层面上和runtime系统进行交互:
    (1)通过OC源代码;
    (2)通过Foundation框架的NSObject类定义的方法;
    (3)通过Runtime库函数的直接调用。

    runtime的常见作用主要有以下几类:
    (1)动态交换两个方法的实现;
    (2)动态添加属性;
    (3)实现字典转模型的自动转换;
    (4)发送消息;
    (5)动态添加方法;
    (6)拦截并替换方法;
    (7)实现NSCoding的自动归档和解档。

    2、简述runtime消息传递
    原理:对象根据方法编号SEL去映射的方法列表中查找对应的方法实现。消息传递机制中的核心函数,叫做objc_msgSend()
    调用流程:
    (1)OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象对应的类或其父类中查找方法;
    (2)注册方法编号(这里用方法编号的好处,可以快速查找);
    (3)根据方法编号去查找对应方法;
    (4)找到的只是最终函数的实现地址,根据地址去方法调用区调用对应函数。

    3、runtime消息转发
    我们先来看一个例子!
    声明一个继承于NSObjectPerson类,其内部未添加任何属性和方法。在ViewController中这样调用:

    - (void)viewDidLoad {
        [super viewDidLoad];
        Person * p = [Person new];
        [p performSelector:@selector(run) withObject:nil];
    }
    

    很明显,运行时项目会崩溃:

    -[Person run]: unrecognized selector sent to instance 0x170004a70
    

    在编译期向类发送了其无法理解的消息并不会报错,因为在运行期可以继续想类中添加方法,所以编译器在编译时还无法确定类中到底有没有这个方法的实现。当对象接收到无法解读的消息后,就会启动“消息转发”机制。消息转发机制中的核心函数,叫做_objc_msgForward消息转发主要分为两大阶段:
    (1)动态方法解析
    第一阶段先征询接收者所在的类,看其是否能动态添加方法,以处理当前这个未知的选择器方法。
    对象在收到无法解读的消息后,首先将调用其所属类的这个类方法:+ (BOOL)resolveInstanceMethod:(SEL)sel。加入尚未实现的方法不是实力方法而是类方法,那么运行期系统会调用方法:+ (BOOL)resolveClassMethod:(SEL)sel。使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。
    继续上面的例子说明:在Person类中动态添加方法实现

    #import "Person.h"
    #import <objc/message.h>
    
    @implementation Person
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(run)) {
            class_addMethod([self class], sel, (IMP)run, "v@:");
            return YES;
        }
        return [super instanceMethodForSelector:sel];
    }
    
    void run(id self, SEL _cmd) {
        NSLog(@"Person跑了!");
    }
    

    运行结果为:

    Person跑了!
    

    (2)涉及完整的消息转发机制
    第二阶段又细分为两小步。
    (2.1)备用接收者
    首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切正常。该步骤对应的处理方法为:- (id)forwardingTargetForSelector:(SEL)aSelector
    继续上面的例子说明:声明一个继承于NSObjectPeople类,其内部添加run方法:

    #import "People.h"
    
    @implementation People
    
    - (void)run {
        NSLog(@"哈哈哈People跑了!");
    }
    
    @end
    

    Person类中实现如下:

    #import "Person.h"
    #import <objc/message.h>
    #import "People.h"
    
    @implementation Person
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        People * p = [People new];
        if ([p respondsToSelector:aSelector]) {
            return p;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    

    运行结果为:

    哈哈哈People跑了!
    

    (2.2)完整的消息转发机制
    若没有备用接收者,则运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会处理这条消息。该步骤会调用下列方法来转发消息- (void)forwardInvocation:(NSInvocation *)anInvocation。在调用该方法之前会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法来获取这个选择器的方法签名,然后在forwardInvocation:方法中就可以通过anInvocation拿到相应信息做处理。
    继续上面的例子说明:

    #import "Person.h"
    #import <objc/message.h>
    #import "People.h"
    
    @implementation Person
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if (aSelector == @selector(run)) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        if (anInvocation.selector == @selector(run)) {
            [anInvocation invokeWithTarget:[People new]];
        }
    }
    
    @end
    

    运行结果为:

    哈哈哈People跑了!
    

    注意:接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果该类的实例稍后还收到同名选择器方法,那么根本无需启动消息转发流程。若想在第三步里把消息转给别的接收者,还不如把转发操作提前到第二步,因为第三步只是修改了调用目标,放在第二步执行会更简单,不然还得创建并处理完整的NSInvocation

    4、+load 和 +initialize 方法
    在OC中,runtime会自动调用+load+initialize方法。

    (1)+load方法
    +load方法是在类被加载进内存的时候调用的,也就是一定会被调用,而且通常情况下只会调用一次。
    它有一个非常重要的特性,就是子类、父类和category中的+load方法的实现是被区别对待的。换句话说,在OC的runtime自动调用+load方法时,category中的+load方法并不会对主类中的+load方法造成覆盖,类和category都执行。因此大部分情况下,我们都将Method Swizzling逻辑的实现放在+load方法中。

    (2)+initialize方法
    +initialize方法是在类或者其子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用,也就是说+initialize方法是以懒加载的方式被调用的,在收到第一条消息前,可能永远不会被调用。而且它category中的实现会覆盖类中的方法,只执行category的实现。

    (3)PS:补充
    category中用@property描述的对象,只会生成set、get方法的声明,不会生成方法的实现,也不会生成下划线成员属性。
    注意:category中一般只能添加方法,不能增加成员变量。但如果一定在类别中添加成员变量,可以通过runtime实现settergetter方法,即objc_setAssociatedObjectobjc_getAssociatedObject

    5、易混淆的概念
    (1)isa
    每个对象内部都有一个isa指针,指向它的真实类型,即找到这个对象所属的类。所以根据这个指针就能知道将来调用哪个类的方法。
    PS:KVO的实现原理就是将观察对象的isa指针指向一个中间类而不是真实类型。

    当一个对象的实例方法被调用的时候,会通过isa找到相应的类,然后在该类的class_data_bits_t中去查找方法。class_data_bits_t是指类对象的数据区域,在该数据区域内查找相应方法的对应实现。
    对象的实例方法调用时,通过对象的isa在类中获取方法的实现。
    类对象的类方法调用时,通过类的isa在元类中获取方法的实现。
    元类:meta-class,它存储着一个类的所有方法。

    (2)IMP
    IMP是一个函数指针,指向的是这个函数的具体实现。在runtime中,消息传递和转发的目的就是为了找到IMP,并执行函数。

    (3)Ivar
    Ivar:成员变量,实例变量,以下划线开头或property属性。
    class_copyPropertyList:获取类里面属性
    class_copyIvarList:获取类中所有的成员变量
    这里注意:只要有成员变量,就不会漏掉属性;如果只有属性,可能会漏掉成员变量。

    (4)_cmd
    SEL类型的一个变量,OC函数的前两个隐藏函数为self和_cmd。

    相关文章

      网友评论

        本文标题:简述runtime

        本文链接:https://www.haomeiwen.com/subject/pxgafftx.html