Runtime运行时机制

作者: Mister志伟 | 来源:发表于2017-12-11 18:09 被阅读125次

    想深入理解Objective-C这门动态语言就不得不深入理解下它的“动态”是如何实现的。早先拜读过《Effective Objective-C 2.0》就让我更深入的窥探到OC运行时特别之处,本文当中也有部分内容借鉴自这本经典著作。第四届互联网大会的项目也完成了,年底闲来无事整理写些总结。

    动态语言是相对于静态语言如C语言区别而言的。C语言在编译期就能决定了运行时应该调用的函数,函数地址实际上是硬编码在指令之中的。而OC在编译期甚至不知道对象的类型,需要在运行时处理,当然它的底层也都是转化为C函数调用。运行时实际上决定了OC最终的编程实现,即什么类的对象执行什么函数,而且这个执行调用是可以修改的,这也是运行时吸引人的地方。

    运行时的调用有3种方式
    * 第一种是系统底层封装实现的,所有OC的代码就会调用,那就是消息传递机制。

    id value = [someObj methodName:parameter];
    // 编译期OC转化为标准C函数
    id value = objc_msgSend(someObj,@selector(methodName:),parameter);
    

    objc_msgSend消息传递机制中的核心函数(实际上是四种objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stret,其他三种在处理一些“边界情况”的时候会用到,可查阅《Effective Objective-C 2.0》第45页,这篇文章也有提及 ),它会根据对象即someObj和它的方法名来调用合适的方法完成完整的函数调用实现。在查询方法名时,它会首先在someObj的“方法列表”中查找,找不到就沿着它的继承体系向上找,如果都没有那就会看到调试时控制台提示的错误包含一句[__ClassName methodName] unrecognized selector sent to instance xxxxsomeObj所属的__ClassName类找不到methodName这个对象方法,否则就可以正常运行了。如此看来,方法调用似乎每次都需要查表效率很低,其实不然,objc_msgSend会将匹配结果缓存到“快速映射表”(fast map)里,每个类都有这样一块缓存,下次再调用方法就直接可在映射表里找了。

    • 第二种是NSObjec这个基类特有的几个调用方法,能做类型判断或者查看是否有响应函数的这些方法都是运行时机制的方法。
    -class方法返回对象的类;
    -isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
    -respondsToSelector: 检查对象能否响应指定的消息;
    -conformsToProtocol:检查对象是否实现了指定协议类的方法;
    -methodForSelector: 返回指定方法实现的地址。
    
    • 第三种就是直接调用Runtime函数库了,稍后在实际应用中会介绍到。

    runtime可以做什么

      1. 动态方法添加

    如上所述,在开发中偶尔会有在消息转发过程中找不到调用方法而导致程序闪退,为了用户体验,闪退是不能允许的,所以我们需要利用运行时来杜绝因这个问题而导致的闪退,而转化为弹出其他报错提示,并把日志记录到后台中方便我们做进一步的程序完善。

    [__ClassName methodName] unrecognized selector sent to instance xxxx这段异常信息是由NSObjectdoesNotRecognizeSelector:方法所抛出的。但并不是拦截这个方法做处理防止闪退,因为这个方法只是帮助打印提示信息的。

    消息转发分为两个阶段,第一阶段是沿着继承体系查找是否能动态添加方法,以处理当前这个未知的方法,叫“动态方法解析”,第二阶段涉及“完整的消息转发机制”,如果第一阶段运行完,那方法接收者(如上边例子中的someObj)就无法再动态添加方法来响应这个找不到的方法了。此时运行时系统会请求接收者用其他手段来处理与消息有关的方法调用,这里又细分为2小步。首先请接收者看看有没有其他对象能处理这条消息,如果有,那么一切如常。若没有,则会启动完整的消息转发机制,运行时系统会把与消息有关的全部细节都封装到NSInvocation对象中(NSInvocation的使用),再给接收者最后一次机会,让它来设法解决这条消息。

    动态方法解析:
    在对象收到无法解读的消息后,首先将调用其所属类的下列类方法:
    + (BOOL)resolveInstanceMethod:(SEL)sel
    该方法的参数就是那个未知的方法,其返回值Boolean类型,表示这个类是否能新增一个实例方法类处理这个方法。如果未知的方法不是对象方法而是类方法,那么调用的就是+ (BOOL)resolveClassMethod:(SEL)sel这个方法了。
    例如:
    someObj调用了未实现的实例方法callMethod,此时我们可以通过重载+ (BOOL)resolveInstanceMethod:(SEL)sel来处理这个未知方法。

    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == NSSelectorFromString(@"callMethod")) {
            /*
             * IMP 是编译期生成的函数指针
             * class_addMethod 函数完成向特定类添加特定方法实现的操作
            */
            class_addMethod(self,sel,(IMP)callMethodTest,"chart");
            return YES;
        }
    
        return [super resolveClassMethod:sel];
    }
    
    void callMethodTest (id self ,SEL _cmd){
        NSLog(@"---callMethodTest----");
    }
    

    这种处理方式也常用来处理@dynamic修饰的属性,因为使用@dynamic就是告诉编译器,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法,我们会为这个属性动态提供存取方法。

    注意:我们并不能重载+ (BOOL)resolveInstanceMethod:(SEL)sel使返回值直接为YES,这样会让我们不知道哪里出了问题,因为我们不能通过SEL来获取方法信息。

    • 2.动态添加属性和判断属性类型

    动态添加属性:
    一般来说分类(category)中是不支持添加属性的,但有时候确实需要添加,那么就可以通过 objc/runtime.h 库中的一些函数来实现。在AFNetworkingMasonrySDWebImage等常用框架中都大量用到了这种方式。
    栗子:

    #import <Foundation/Foundation.h>
    
    @interface NSObject (ExchangeMethod)
    
    @property (strong, nonatomic) NSString *name;
    
    @end
    
    #import "NSObject+ExchangeMethod.h"
    #import <objc/runtime.h>
    
    #define NameKey @"nameKey"
    
    @implementation NSObject (ExchangeMethod)
    
    - (void)setName:(NSString *)name{
        // 将属性同对象关联
        objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name{
        // 取出 对应Key关联的对象属性
        return objc_getAssociatedObject(self, NameKey);
    }
    
    @end
    

    属性类型判断:
    类型判断常见的使用场景就是数据解析--字典转模型。
    获取属性列表的方式有两种:

    // 第一种
        unsigned int count;
        
        objc_property_t *properties = class_copyPropertyList(self.class, &count);
        
        NSMutableArray *array = [NSMutableArray array];
        
        for (int i =0; i< count ; i++) {
            objc_property_t pro = properties[I];
            const char *name = property_getName(pro);
            const char *attributes = property_getAttributes(pro);
            NSString *property = [[NSString alloc] initWithUTF8String:name];
            [array addObject:property];
             NSLog(@"attributes : %s,  name: %s",attributes,name);
        }
    
    // 第二种
        unsigned int count;
        /*
         *参数1:类名
         *参数2:传入无符号整型的内存地址,当读取到成员变量的数量时,会给这个值赋值
         *返回值:Ivar * :是一个指针类型,相当于数组,里边装着Ivar
         */
        
        Ivar *ivars = class_copyIvarList([UIView class],&count);
        
        for (int i=0; i < count; i++) {
            Ivar ivar = ivars[I];
            // 获取属性名字,调用函数ivar_getName(ivar)获取
            NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 获取属性类型,调用函数ivar_getTypeEncoding(ivar)获取
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            NSLog(@"type : %@,  name: %@",type,name);
        }
    
    /// An opaque type that represents an instance variable.
    /*
    Ivar 是表示成员变量的类型
    */
    typedef struct objc_ivar *![Ivar.png](http:https://img.haomeiwen.com/i308319/1ad920412e90db1d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    ;
    
    /// An opaque type that represents an Objective-C declared property.
    /*
    objc_property_t 是表示一个Objective-C声明的属性
    */
    typedef struct objc_property * objc_property_t;
    

    两者都可以获取属性名称和类型,信息详细程度不一样。

    Ivar.png objc_property_t.png

    objc_property_t打印的属性的特性字符串说明,通过property_getAttributes(objc_property_t _Nonnull property)获取查看

    //特性
      typedef struct {
          const char *name;           //特性名称
          const char *value;          //特性的值
      } objc_property_attribute_t;
    

    特性编码 具体含义

    R readonly
    C copy
    & retain
    N nonatomic
    G(name) getter=(name)
    S(name) setter=(name)
    D @dynamic
    W weak
    P 用于垃圾回收机制
    

    详细参见
    一般获取属性信息用第一种,YYModel和MJExtension 框架中都有用到。

    • 3.方法交换
      OC对象在收到消息后,究竟调用哪种方法是在运行时才能解析决定的。而在运行时我们还可以新增、修改或者交换执行方法,也叫“方法调配”即method swizzling

    类的“方法列表”中会把方法名映射到相关的方法实现上,通过“动态消息派发系统”找到对应的调用方法。这些方法均已函数指针的形式来表示,即IMP。比如:someObj对象可以响应makeNamemakeHeightmakeSex等方法,这张表中的每个方法都映射到不同的IMP上,如下图。

    someObj对象的方法映射表.png

    OC在运行时系统提供的几个API能用来操作这张表。
    method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)可以用来做方法交换。

    +(void)load{
        Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"makeName"));
        Method m2 = class_getInstanceMethod(self, @selector(testMakeName));
        
        method_exchangeImplementations(m1, m2);
    }
    

    不过,在实际开发中,直接交换方法的意义并不大,每一个方法都应该对应自己的实现。但是,为既有方法添加新功能是比较实用的。
    栗子:
    NSString的获取小写字符串方法lowercaseString,我们要打印信息,那我们可以这样写

    +(void)load{
        Method m1 = class_getInstanceMethod(self, NSSelectorFromString(@"lowercaseString"));
        Method m2 = class_getInstanceMethod(self, @selector(mcLowercaseString));
        
        method_exchangeImplementations(m1, m2);
    }
    
    - (NSString *)mcLowercaseString{
        NSString *lowercase = [self mcLowercaseString];
        NSLog(@"%@ => %@",self,lowercase);
        return lowercase;
    }
    

    这段代码看似会死循环,其实不然,因为两个方法名指向了对方的函数指针IMP,所以[self mcLowercaseString];实际上是调用的lowercaseString。通过这种方式,我们可以为那些系统黑盒方法增加日志打印功能,非常有助于调试使用。一般很少有人用这个特性永久修改某各类的功能,而且若滥用的话,反而会让代码不易读难于维护。

    关于分类category美团技术博客也有一篇点击查看
    喜欢就点个赞呗!
    欢迎大家提出更好的改进意见和建议,从搬砖到设计建筑的路上,你我同行!

    相关文章

      网友评论

        本文标题:Runtime运行时机制

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