【iOS】Runtime解读

作者: Yaso | 来源:发表于2017-07-11 00:33 被阅读482次

    这段时间在公司要做一个组件开发,需要用到OC Runtime特性的地方很多,于是在以前的了解上又恶补了一下相关知识,以下是自己的一些总结。如果有不对的地方,欢迎大家及时指出.

    一、Runtime 是什么?

    Runtime机制是Objective-C的一个重要特性,是其区别于C语言这种静态语言的根本,C语言的函数调用会在编译期确定好,在编译完成后直接顺序执行。而OC是一门动态语言,函数调用变成了消息发送(msgSend),在编译期不能确定调用哪个函数,所以Runtime就是解决如何在运行期找到调用方法这样的问题。

    二、类的结构定义

    要想理解清楚Runtime,首先要清楚的了解类的结构, 因为Objective-C 是面向对象语言,所以可以说 OC 里“一切皆对象”,首先要牢记这一点。众所周知一个实例instance 是一个类实例化生成的对象(以下简称实例对象),那各个不同的类呢?实际上各个不同的类本质上也是各个不同的对象(以下简称类对象)

    先来看张图:

    实例和类的构造说明
    上图中:
    superClass:类对象所拥有的父类指针,指向当前类的父类.
    isa: 实例和类对象都拥有的指针,指向所属类,即当前对象由哪个类构造生成.
    

    所以从上图我们可以得出以下几点结论:

    • 实例对象的isa指针指向所属类,所属类的isa指针指向元类(metaClass) .
    • metaClass也有isa 和superClass 指针,其中isa指针指向Root class (meta) 根元类.
    • superClass 指针追溯整个继承链,自底向上直至根类 (NSObject或NSProxy) .
    • Root class (meta) 根元类的superClass指针指向根类
    • 根类和根元类的isa 指针都指向Root class (meta) 根元类

    好,到这里我们清楚的了解了实例和类在内存中的布局构造,那么接下来我们来看一下类的结构定义,在 objc/runtime.h中,类由objc_class结构体构造而成,如下是在objc/runtime.h中,类的结构的定义:

    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY; // isa指针  指向所属类
    
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE; // 父类指针
        const char *name                                         OBJC2_UNAVAILABLE; // 类名
        long version                                             OBJC2_UNAVAILABLE;
        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;
    /* Use `Class` instead of `struct objc_class *` */
    

    结合上述结构定义和Runtime提供的一系列方法,我们可以轻而易举的获取一个类的相关信息,譬如:成员变量列表ivars、实例方法列表methodLists、遵循协议列表protocols和属性列表propertyList等。

    下面简单的列举一下获取相关列表的方法:

      #import <objc/runtime.h>
    
        例如:获取UIView类的相关信息
        id LenderClass = objc_getClass("UIView");
        unsigned int outCount, i;
        
        //获取成员变量列表
        Ivar *ivarList = class_copyIvarList(LenderClass, &outCount);
        for (i=0; i<outCount; i++) {
            Ivar ivar = ivarList[i];
            fprintf(stdout, "Ivar:%s \n", ivar_getName(ivar));
        }
    
        //获取实例方法列表
        Method *methodList = class_copyMethodList(LenderClass, &outCount);
        for (i=0; i<outCount; i++) {
            Method method = methodList[i];
            NSLog(@"instanceMethod:%@", NSStringFromSelector(method_getName(method)));
        }
        
        //获取协议列表
        __unsafe_unretained Protocol **protocolList = class_copyProtocolList(LenderClass, &outCount);
        for (i=0; i<outCount; i++) {
            Protocol *protocol = protocolList[i];
            fprintf(stdout, "protocol:%s \n", protocol_getName(protocol));
        }
        
        //获取属性列表
        objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
        for (i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            //第二个输出为属性特性,包含类型编码、读写权限等
            fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
        }
        
        //注意释放
        free(ivarList);
        free(methodList);
        free(protocolList);
        free(properties);
    

    通过上面的列子,我们不难发现一个对象所拥有的实例方法,都注册在其所属类的方法列表methodLists中,同理你会发现所有的类方法,都注册在这个类所属元类的方法列表中。

    三、Method

    既然我们已经清楚的知道不同类型的方法都保存在相对应的方法列表methodLists中,那方法列表中所存储的方法的结构又是怎样的呢?弄清这一点对我们下面理解方法调用很有帮助。

    好,我们回头看下上面类的结构定义中方法列表的定义:

    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE; // 方法地址列表
    

    不难发现methodLists是一个指向objc_method_list 结构体类型指针的指针,那objc_method_list 的结构又是怎样的呢?在runtime.h里搜索其定义:

    struct objc_method {
        SEL method_name     //方法id                                    OBJC2_UNAVAILABLE;
        char *method_types  //各参数和返回值类型的typeEncode                                     OBJC2_UNAVAILABLE;
        IMP method_imp      //方法实现                                   OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    
    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;
    }  
    

    这就很清楚了,objc_method_list中有objc_method,而objc_method里有SEL 和 IMP 这两个关键点:

    1. SEL:在编译时期,根据根据方法名字生成的唯一int标识(会转为char*字符串使用),可以将其理解为方法的ID。
    2. IMP:方法实现、函数指针,该指针指向最终的函数实现。

    SEL 结构如下:

    typedef struct objc_selector *SEL;
    
    struct objc_selector {
          char *name;                       OBJC2_UNAVAILABLE;
          char *types;                      OBJC2_UNAVAILABLE;
      };
    

    注:既然SEL 和 IMP 一一对应,那么方法列表中会存在两个SEL相同的方法吗?
    答案是:会的。因为methodLists方法列表是一个数组,当我们给一个类添加一个分类,并在分类中重写这个类的方法时,编译后会发现方法列表中有两个SEL相同的method,对应两个不同的IMP,那么当调用这个方法时,会调用执行那个IMP呢?答案是分类的那个,原理会在以后的文章中补上。

    相信到这里,你已经大致猜到了方法调用的过程,其实一个方法的调用就是通过方法名生成的SEL,到相应类的方法列表methodLists中,遍历查找相匹配的IMP,获取最终实现并执行的过程。当然OC实现这些过程,还依赖于一个Runtime的核心:objc_msgSend

    四、objc_msgSend

    objc_msgSend定义:

    /** 
     * Sends a message with a simple return value to an instance of a class.
     * 
     * @param self A pointer to the instance of the class that is to receive the message.
     * @param op The selector of the method that handles the message.
     * @param ... 
     *   A variable argument list containing the arguments to the method.
     * 
     * @return The return value of the method.
     */
    void objc_msgSend(void /* id self, SEL op, ... */ )
    

    OC中所有的方法调用最终都会走到objc_msgSend去调用,这个方法支持任意返回值类型,任意参数类型和个数的函数调用。

    支持所有的函数调用??这不是违背了Calling Convention(“调用规则”)?这样最后底层执行的时候能够正确取参并正确返回吗?难道不会报错崩溃?答案是当然不会,因为objc_msgSend是用汇编写的,调用执行的时候,直接执行自己的汇编代码就OK了,不再需要编译器根据相应的调用规则生成汇编指令,所以它也就不需要遵循相应的调用规则。后续会写一篇Libffi相关的文章表述一下。

    当一个对象调用[receiver message]的时候,会被改写成objc_magSend(self,_cmd,···),其中self 是指向消息接受者的指针,_cmd 是根据方法名生成的SEL,后面是方法调用所需参数。执行过程中就会拿着生成的SEL,到消息接受者所属类的方法列表中遍历查找对应的IMP,然后调用执行。可以看出OC的方法调用中间经历了一系列过程,而不是像C一样直接按地址取用,所以我们可以利用这一点,在消息处理的过程中对消息做一些特殊处理,譬如:消息的转发,消息的替换,消息的防崩溃处理等。

    objc_msgSend 调用流程:

    • 检查SEL是否应该被忽略
    • 检查target 是否为空,为空则忽略该消息
    • 查找与SEL相匹配的IMP
      • 如果是调用实例方法,则通过isa指针找到实例对象所属类,遍历其缓存列表及方法列表查找对应IMP,如果找不到则去super_class指针所指父类中查找,直至根类.
      • 如果是调用类方法,则通过isa指针找到类对象所属元类,遍历其缓存列表及方法列表查找对应IMP,如果找不到则去super_class指针所指父类中查找,直至根类.
    • 如果都没找到,则转向拦截调用,进行消息动态解析
    • 如果没有覆写拦截调用相关方法,则程序报错:unrecognized selector sent to instance.

    注:上述过程中的缓存列表就是类结构定义中的 struct objc_cache *cache 因为OC调用要经过一系列的流程比较慢,所以引入了缓存列表机制,调用过的方法会存到缓存列表中,这一点极大的提高了OC函数调用的效率。

    五、动态消息解析

    如四所述,如果在objc_msgSend调用的前3个步骤结束,还未找到SEL 对应 IMP,则会转向动态消息解析流程,也可简称为拦截调用,所谓拦截调用就是在消息无法处理 unrecognized selector sent to instance. 之前,我们有机会覆写NSObject 的几个方法来处理消息,这也正是OC 动态性的体现。

    这几个方法分别是:

    /* 所调用类方法是否为动态添加 */
    + (BOOL)resolveClassMethod:(SEL)sel;
    /* 所调用实例方法是否为动态添加 */
    + (BOOL)resolveInstanceMethod:(SEL)sel;
    /* 将消息转发到其他目标对象处理 */
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    /* 返回方法签名 */
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    /* 在这里触发调用 */
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    

    同时来看张网上流转较广的关于动态消息解析的流程图:


    动态消息解析流程图

    流程说明:

    1. 通过resolveClassMethod:resolveInstanceMethod: 判断所调用方法是否为动态添加,默认返回NO,返回YES则通过 class_addMethod 动态添加方法,处理消息。
    2. forwardingTargetForSelector: 将消息转发给某个指定的目标对象来处理,效率比较高,如果返回空则进入下一步。
    3. methodSignatureForSelector: 此方法用于方法签名,将调用方法的参数类型和返回值进行封装并返回,如果返回nil,则说明消息无法处理unrecognized selector sent to instance.,正常返回则进入forwardInvocation: 此步拿到的anInvocation,包含了方法调用所需要的全部信息,在这里可以修改方法实现,修改响应对象,然后invoke 执行,执行成功则结束。失败则报错unrecognized selector sent to instance.

    六、Runtime相关实践

    经过上面的讲解,相信大家已经对Runtime 的原理有了比较清晰的理解,那么下面我们来看看Runtime的相关应用吧。

    - 动态添加方法

    如果我们调用一个方法列表中不存在的方法newMethod:,根据上述的动态消息解析流程可知,会先走进resolveClassMethod:resolveInstanceMethod:,假设消息接受者receiver为一个实例对象:

    /* 调用一个不存在的方法 */
    [receiver performSelector:@selector(newMethod:) withObject:@"add_newMethod_suc"];
    

    层层查找方法列表均为找到对应IMP,转向动态消息解析,此时需要在目标对象的类里重写resolveInstanceMethod:

    void newMethod(id self, SEL _cmd, NSString *string){
        NSLog(@"%@", string);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
         if (sel == @selector(newMethod)) {
            // 参数依次是:给哪个类添加方法、方法ID:SEL、函数实现:IMP、方法类型编码:types
            class_addMethod(self, @selector(newMethod), newMethod, "v@:@");
            return YES;
         }
        return [super resolveInstanceMethod:sel];
    }
    
    /**class_addMethod 
     * Adds a new method to a class with a given name and implementation.
     * 
     * @param cls The class to which to add a method.
     * @param name A selector that specifies the name of the method being added.
     * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
     * @param types An array of characters that describe the types of the arguments to the method. 
     * 
     * @return YES if the method was added successfully, otherwise NO 
    */
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 
    
    - 方法替换 & 关联对象

    关于这两个方面,下面通过一个UIButton的防重点击的实现来说明:

    #import <UIKit/UIKit.h>
    
    @interface UIButton (IgnoreEvent)
    // 按钮点击的间隔时间
    @property (nonatomic, assign) NSTimeInterval clickDurationTime;
    
    @end
    
    #import "UIButton+IgnoreEvent.h"
    #import <objc/runtime.h>
    
    // 默认的点击间隔时间
    static const NSTimeInterval defaultDuration = 0.0001f;
    
    // 记录是否忽略按钮点击事件,默认第一次执行事件
    static BOOL _isIgnoreEvent = NO;
    
    // 设置执行按钮事件状态
    static void resetState() {
        _isIgnoreEvent = NO;
    }
    
    @implementation UIButton (IgnoreEvent)
    
    @dynamic clickDurationTime;
    
    + (void)load {
        SEL originSEL = @selector(sendAction:to:forEvent:);
        SEL mySEL = @selector(my_sendAction:to:forEvent:);
        
        Method originM = class_getInstanceMethod([self class], originSEL);
        IMP originIMP = method_getImplementation(originM);
        const char *typeEncodinds = method_getTypeEncoding(originM);
        
        Method newM = class_getInstanceMethod([self class], mySEL);
        IMP newIMP = method_getImplementation(newM);
    
        // 方法替换
        if (class_addMethod([self class], originSEL, newIMP, typeEncodinds)) {
            class_replaceMethod([self class], mySEL, originIMP, typeEncodinds);
        } else {
            method_exchangeImplementations(originM, newM);
        }
    }
    
    - (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
        
        if ([self isKindOfClass:[UIButton class]]) {
            
            //1. 按钮点击间隔事件
            self.clickDurationTime = self.clickDurationTime == 0 ? defaultDuration : self.clickDurationTime;
            
            //2. 是否忽略按钮点击事件
            if (_isIgnoreEvent) {
                //2.1 忽略按钮事件
                return;
            } else if(self.clickDurationTime > 0) {
                //2.2 不忽略按钮事件
                
                // 后续在间隔时间内直接忽略按钮事件
                _isIgnoreEvent = YES;
                
                // 间隔事件后,执行按钮事件
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.clickDurationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    resetState();
                });
                
                // 发送按钮点击消息
                [self my_sendAction:action to:target forEvent:event];
            }
        } else {
            [self my_sendAction:action to:target forEvent:event];
        }
    }
    
    #pragma mark - associate
    // 关联对象
    - (void)setClickDurationTime:(NSTimeInterval)clickDurationTime {
        objc_setAssociatedObject(self, @selector(clickDurationTime), @(clickDurationTime), OBJC_ASSOCIATION_RETAIN_ASSIGN);
    }
    
    - (NSTimeInterval)clickDurationTime {
        return [objc_getAssociatedObject(self, @selector(clickDurationTime)) doubleValue];
    }
    
    @end
    
    

    上述方法交换的代码已经很清楚了,简单说下关联对象的两个函数:

    • 设置关联对象 :objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    1. id object:给谁设置关联对象
    2. const void *key: 关联对象唯一的key
    3. id value: 关联对象的值
    4. objc_AssociationPolicy policy:关联策略,有以下几种:

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0, /< Specifies a weak reference to the associated object. /
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /
    < Specifies a strong reference to the associated object.
    * The association is not made atomically. /
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /
    < Specifies that the associated object is copied.
    * The association is not made atomically. /
    OBJC_ASSOCIATION_RETAIN = 01401, /
    < Specifies a strong reference to the associated object.
    * The association is made atomically. /
    OBJC_ASSOCIATION_COPY = 01403 /
    < Specifies that the associated object is copied.
    * The association is made atomically. */
    };
    ```

    • 获取关联对象 :id objc_getAssociatedObject(id object, const void *key)
    1. id object:获取谁的关联对象
    2. const void *key: 根据key获取相应的关联对象值

    Runtime的相关应用还有很多很多,大家可以在以后的开发过程中慢慢探索。

    综上,就是这次对Runtime的一些总结,对于Runtime整体来说可能只是很小的一部分,但是对于大家理解一些常见的Runtime使用应该还是有所帮助的,鉴于苹果API一直在更新和自己能力尚浅,文章中如有错误或不妥之处,还请大家及时指出。

    相关文章

      网友评论

      • a93597be0215:objc_setAssociatedObject(self, @Selector(clickDurationTime), @(clickDurationTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        这里是不是应该使用OBJC_ASSOCIATION_ASSIGN,因为NSTimeInterval也属于基本类型吧
        Yaso:多谢指出,已更正
      • 草莓姜:很详细,不错~~
        Yaso:多谢支持~
      • 125788e11c64:不错,打赏一个
      • 陈逸辰:楼主文章写得很好,思路清晰,学习了~
        Yaso:@陈逸辰 多谢支持~

      本文标题:【iOS】Runtime解读

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