Runtime详解

作者: 六月的第三天 | 来源:发表于2019-03-21 09:35 被阅读0次

    前言

    上面文章主要讲了iOS中类、实例、和元类的概念以及在runtime中是如何定义以及之间的联系,这篇文章接着把runtime的整个概念给讲清楚,包括runtime在实战中的应用。

    各个数据项的结构体定义

    struct object_class{
        Class isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
         Class super_class                        OBJC2_UNAVAILABLE;  // 父类
         const char *name                         OBJC2_UNAVAILABLE;  // 类名
         long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
         long info                                OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
         long instance_size                       OBJC2_UNAVAILABLE;  // 该类的实例变量大小
         struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表
         struct objc_method_list *methodLists     OBJC2_UNAVAILABLE;  // 方法定义的链表
         struct objc_cache *cache                 OBJC2_UNAVAILABLE;  // 方法缓存
         struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE;  // 协议链表
    #endif
    }OBJC2_UNAVAILABLE;
    

    上篇文章我们讲了isa指针是如何连接实例、类、和元类的,以及super_class指向的什么,其实这些上面都注释的很清楚,接下来就来说说下面几个数据项的定义以及应用。

    至于定义,比如成员变量链表objc_ivar_list以及方法列表objc_method_list的结构体定义都可以在runtime.h中找到,这里就不在重复展示了,具体到用的时候再去逐个分析。

    SEL : 又叫选择器,是表示一个方法的selector的指针,其定义如: typedef struct objc_selector *SEL;
    方法的selector用于表示运行时方法的名字, 编译器会根据方法的名称和参数序列生成唯一的一个标识符 SEL(int类型的一个地址),这也是不允许方法重名的原。

    本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。 通过下面三种方法可以获取SEL

    • sel_registerName函数
    • Objective-C编译器提供的@selector()
    • NSSelectorFromString()方法

    IMP : 实际上是一个函数指针,指向方法实现的地址。id (*IMP)(id, SEL,...)
    第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针) 第二个参数:是方法选择器(selector)
    接下来的参数:方法的参数列表。

    Method : 表示类定义中的方法

    typedef struct objc_method *Method
    struct objc_method{
        SEL method_name      OBJC2_UNAVAILABLE; // 方法名
        char *method_types   OBJC2_UNAVAILABLE;
        IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
    }
    

    方法调用的流程

    oc中方法调用会有一个中间转换,转换为一个c函数才能在被runtime执行

    [objc methodName] ==> objc_msgSend(id self, SEL op, ...)
    

    objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象不为空的时候,就去方法缓存列表(objc_cache)里面找,找到了就分发,否则:

    1. 如果没找到就要去方法列表里找objc_method_list,如果找到就缓存起来
    2. 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector 否则
    3. 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
    4. 进行消息转发

    方法缓存的设计

    缓存方法是苹果在代码设计上的一个方法,很好的避免了大量调用方法造成的性能消耗。

    struct objc_cache {
        //可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
        unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        //实际占用bucket 因为缓存采用的是散列表,所以会有空的bucket
        unsigned int occupied                                    OBJC2_UNAVAILABLE;
        //指向Method数据结构指针的数组,这个数组的总数不能超过mask+1,数组会随着时间增长。
        Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
    };
    

    问:方法的缓存全部放到struct objc_cache *cache这里,那么我们调用父类的方法的时候会把方法缓存到子类中吗?
    答:会缓存。而当用一个父类对象去调用那个方法的时候,也会在父类的cache里缓存一份.

    问:为什么方法列表不做成哈希表,而做成list,然后单独再创建缓存哈希表?
    答:散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。另一个原因是哈希表是有空位的浪费空间。

    消息转发

    既然上面提到了消息转发那就放到这里讲一下到底消息是如何转发的。

    这里我们先明白一个概念就是,消息转发是当一个消息在运行时发送的过程中找不到目标方法的实现地址时候,系统做的一个不就措施,这个措施总共分三个步骤:

    • 动态方法解析

    + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod: 这两个方法作用一样,区别是一个处理实例方法,一个处理类方法

    实例:

    void spareMethod(id obj, SEL _cmd)
    {
        NSString * className = NSStringFromClass([obj class]);
        NSString * selName   = NSStringFromSelector(_cmd);
        NSLog(@"%@:没有实现%@的方法",className,selName);
    }
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if(aSEL == @selector(sendMessage:)){
            class_addMethod([self class], aSEL, (IMP)spareMethod, "v@:"); //动态增加方法
            return YES;
        }
        return [super resolveInstanceMethod:aSEL];
    }
    

    顺便贴一个方法参数的签名含义:

    *          代表  char * 
    char BOOL  代表  c
    :          代表  SEL 
    ^type      代表  type *
    @          代表  NSObject * 或 id
    ^@         代表  NSError ** 
    #          代表  NSObject 
    v          代表  void
    
    • 备用接收者
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        NSString *selStr = NSStringFromSelector(aSelector);
        
        if ([selStr isEqualToString:@"sendMessage:"]) {
            return [[Other alloc] init];        // 这里返回Other类对象,让Other去处理sendMessage消息
        }
        
        return [super forwardingTargetForSelector:aSelector];
    }
    
    • 完整转发
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSString *sel = NSStringFromSelector(aSelector);
        // 判断要转发的SEL
        if ([sel isEqualToString:@"sendMessage:"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"]; //必须要有方法签名
        }
        
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        SEL selector = [anInvocation selector];
        // 新建需要转发消息的对象 这里可以创建多个对象,同时转发
        Other *obj = [[Other alloc] init];
        if ([obj respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:obj];
        }
    }
    

    完整转发有时候可以让object-c 拥有多继承能力.

    在一个函数找不到时,OC提供了三种方式去补救:

    1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
    2. 调用forwardingTargetForSelector让别的对象去执行这个函数
    3. 调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

    如果都不中,调用doesNotRecognizeSelector抛出异常。

    Category类别又是如何定义和实现的

    这里顺便提一下category和extension的区别:

    • extension更像是一个匿名的category,但是和有名字的category是完全两个东西,extension一般用来隐藏类的私有信息,在编译期间是和class一起编译的,是class不可分割的一部分,你必须有一个类的源码才能为一个类添加extension
    • category完全是运行期决定的,但是category是不能添加成员变量和属性的(增加属性可通过runtime技术实现),因为运行期class的布局已经确定,添加成员变量会破坏class的内存布局。category中定义的方法如果和class同名优先级高于class中定义的方法
    //可以看到类别中没有 ivar 字段 也就是不能添加成员变量。
    typedef struct objc_category *Category
    struct objc_category{
         char *category_name                         OBJC2_UNAVAILABLE; // 分类名
         char *class_name                            OBJC2_UNAVAILABLE;  // 分类所属的类名
         struct objc_method_list *instance_methods   OBJC2_UNAVAILABLE;  // 实例方法列表
         struct objc_method_list *class_methods      OBJC2_UNAVAILABLE; // 类方法列表
         struct objc_protocol_list *protocols        OBJC2_UNAVAILABLE; // 分类所实现的协议列表
    }
    

    runtime 实战应用

    方法交换(Method Swizzling)

    方法交换一定要在+load()方法中执行,确保在类初始化中一定能执行,并且搭配dispatch_one 防止多线程重复执行

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL originalSelector = @selector(methodA);
            SEL swizzledSelector = @selector(xxx_methodA);
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            method_exchangeImplementations(originalMethod, swizzledMethod);
        });
    }
    

    通过方法交换的这种黑魔法你能够实现hook一些系统的方法,实现中间件的功能

    给category添加属性

    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy)
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
                             
    

    我们知道category是不能添加成员变量和属性的,但是通过上面runtime方法就能实现增加属性(其实还是增加set和get方法),至于怎么使用就实际开发中的场景需求了

    给OC语言实现多继承

    目前网上实现好多文章给出实现多继承的方案无非就三种:

    • 通过组合实现
    • 通过协议
    • 通过category
    • 消息转发机制

    这里要说的就是上面我们刚介绍过的消息转发,大致思路是:

    假如我们想让C继承自A和B,理论上我们是无法实现的,但是我们可以先让C继承自A,然后C就可以使用A公开的方法了,然后B公开的方法我们C对象不能调用,但是可以再C类里面,通过消息转发来把方法转发给B,让B去实现,从而就实现了C不但可以调用A类的方法,调用B类的方法时,B依然可以响应,不就实现了多继承吗。具体代码其实还是上面消息转发的实现代码,在消息转发中吧处理方法的对象换成你想要的对象就行了(比如B)**

    模型转换

    其实模型转换和自动化归档这些小技巧也都是通过runtime的强大功能实现的,可以节省很多冗余代码,让代码更简练。具体实现可以翻看写实现这种类库的源码。

    相关文章

      网友评论

        本文标题:Runtime详解

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