美文网首页
Runtime简介

Runtime简介

作者: stockholder | 来源:发表于2017-08-11 00:48 被阅读0次
    Runtime 概念

    runtime(简称运行时),是一套纯C(C和汇编写的) 的API。而 OC 就是运行时机制(消息机制)。
    在编译阶段,OC 调用并未实现的函数,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而C语言,函数的调用在编译的时候会决定调用哪个函数,调用未实现的函数就会报错

    runtime 消息机制

    OC方法调用本质:就是用 runtime发送一个消息,每一个 OC 的方法底层必然有一个与之对应的 runtime 方法.
    消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。

    1. 例子:
    创建一个macos工程,就在main.m里写下面简单的代码
    Dog *dog = [[Dog alloc] init];
    [dog run];
    1. 导入 #import <objc/message.h>,因为这个里面包含下面两个
    #include <objc/objc.h>
    #include <objc/runtime.h>
    2.去到 build setting -> 搜索msg ->将Enable Strict Checking of objc_msgSend Calls 改为no 
    否则使用 objc_msgSend 编译出错,因为xcode默认不建议使用
    3.去到main.m所在的目录,在终端用下面命令编译一下
    clang -rewrite-objc main.m
    就会生成一个main.cpp文件
    4.打开该文件看最下面main方法,可以看到编译后的代码就是runtime
    
    1. 使用:
      objc_msgSend(id self, SEL op, ...)
      参数:oc对象,方法编号,其他参数...
    Dog *dog = [[Dog alloc] init];
    [dog run];
    可以写成下面的
    //Class 类类型  就是一个特殊的对象
    Dog *dog = objc_msgSend([Dog class], @selector(alloc));
    dog = objc_msgSend(dog, @selector(init));
    objc_msgSend(dog, @selector(run));
    //
    // 底层的实际写法
    Dog *dog = objc_msgSend(objc_getClass("Dog"),sel_registerName("alloc"));
    dog = objc_msgSend(dog, sel_registerName("init"));
    objc_msgSend(dog, @selector(run));
    
    1. 消息机制方法调用流程
      对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
      OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中根据方法编号(SEL)去查找对应方法,找到只是最终函数实现地址(IMP),根据地址去方法区调用对应函数。
      补充:每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
    runtime 使用场景
    1. 动态交换两个方法的实现(method swizzling)HOOK思想
      需求:给系统的imageNamed添加额外功能(是否加载图片成功)
      方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
      方案二:搞个分类,定义一个能加载图片并且能打印的方法(弊端:不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super,所以要 自己实现一个带有扩展功能的方法.但这样就得改调用的方法,改动大)
      runtime方式实现步骤:
      1.给UIImageView添加分类
      2.自定义并实现带有扩展功能的方法
      3.交换方法
    - (void)viewDidLoad {
        [super viewDidLoad];
        UIImage *image = [UIImage imageNamed:@"123"];
    }
    
    #import <objc/message.h>
    @implementation UIImage (Image)
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
        // 获取方法地址
        Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
        Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
        // 交换方法地址
        if (!class_addMethod([self class], @selector(ln_imageNamed:), method_getImplementation(ln_imageNamedMethod), method_getTypeEncoding(ln_imageNamedMethod))) {
            method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
        }
        });
    }
    // 自己定义的方法
    + (UIImage *)ln_imageNamed:(NSString *)name {
        UIImage *image = [UIImage ln_imageNamed:name];
        if (image) {
            NSLog(@"load image success");
        } else {
            NSLog(@"load image failed");
        }
        return image;
    }
    @end
    

    上面代码执行过程,会先执行load方法,这个时候imageNamed:和ln_imageNamed:就交换了,走到viewDidLoad的 [UIImage imageNamed:@"123"] 时,实际上执行的是ln_imageNamed:,ln_imageNamed:里面又调用ln_imageNamed:,实际上调用的是imageNamed:,这样就根据imageNamed:的返回值来判断。

    屏幕快照 2017-08-10 上午12.43.14.png

    说明以及注意事项:

    • 方法交换为什么写在load方法
      load 把类加载进内存的时候调用,只会调用一次
    • 为了避免Swizzling的代码被重复执行(调用[super load]),利用dispatch_once函数内代码只会执行一次的特性。
    • class_getClassMethod(获取某个类的方法)
      class_getInstanceMethod (获取某个对象的方法)
    • IMP本质上就是函数指针,所以我们可以通过打印函数地址的方式,查看SEL和IMP的交换流程
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
    NSLog(@"%p", method_getImplementation(imageNamedMethod));
    NSLog(@"%p", method_getImplementation(ln_imageNamedMethod));
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
    
    • 使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。而且self没有交换的方法实现,但是父类有这个方法(或者自己有这个方法),这样就会调用父类的方法,结果就不是我们想要的结果了。所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了
    1. runtime结合kvc实现NSCoding的自动归档和解档
      如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,就非常麻烦。
    • 原来的做法
    遵守协议NSCoding
    @property (nonatomic, copy) NSString *name;
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        [aCoder encodeObject:_Name forKey:@"name"];
    }
    - (id)initWithCoder:(NSCoder *)aDecoder{
        if (self = [super init]) {
            self.movieName = [aDecoder decodeObjectForKey:@"name"];
        }
        return self;
    }
    
    • 新做法(主要代码)
    //解档
    - (void)decode:(NSCoder *)aDecoder {
        // 一层层父类往上查找,对父类的属性执行归解档方法
        Class c = self.class;
        while (c &&c != [NSObject class]) {
            
            unsigned int outCount = 0;
            Ivar *ivars = class_copyIvarList(c, &outCount);
            for (int i = 0; i < outCount; i++) {
                Ivar ivar = ivars[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                
                // 如果有实现该方法再去调用
                if ([self respondsToSelector:@selector(ignoredNames)]) {
                    if ([[self ignoredNames] containsObject:key]) continue;
                }
                
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
            free(ivars);
            c = [c superclass];
        }
        
    }
    // 归档
    - (void)encode:(NSCoder *)aCoder {
        // 一层层父类往上查找,对父类的属性执行归解档方法
        Class c = self.class;
        while (c &&c != [NSObject class]) {
            
            unsigned int outCount = 0;
            Ivar *ivars = class_copyIvarList([self class], &outCount);
            for (int i = 0; i < outCount; i++) {
                Ivar ivar = ivars[i];
                // 获取成员变量的名字
                const char *name = ivar_getName(ivar);
                //// C字符串 -> OC字符串
                NSString *key = [NSString stringWithUTF8String:name];
                
                // 如果有实现该方法再去调用
                if ([self respondsToSelector:@selector(ignoredNames)]) {
                    if ([[self ignoredNames] containsObject:key]) continue;
                }
                
                id value = [self valueForKeyPath:key];
                [aCoder encodeObject:value forKey:key];
            }
            free(ivars);
            c = [c superclass];
        }
    }
    
    1. 动态添加方法
      如果一个类方法非常多,因为需要给每个方法生成映射表,实际上只要一个类实现了某个方法,就会被加载进内存,加载类到内存的时候就比较耗费资源。当硬件内存过小的时候,如果我们将每个方法都直接加到内存当中去,但是很久都不用一次,这样就造成了浪费,那如果我想像懒加载一样,先把方法定义好,但是只有当你用的时候我再加载你,这就需要动态添加了。
      当performSelector方法调用某个sel的时候,这时候会到调用对象的+ (BOOL)resolveInstanceMethod:(SEL)sel方法中,如果这里返回是NO,就表示找不到。
    • 看下面的例子
    // 动态添加方法就不会报错
        Person * p = [[Person alloc] init];
        [p performSelector:@selector(eat:) withObject:@"吃过了"];
    
    //下面代码在Person.m里
    #import <objc/runtime.h>
    void addEat(id self, SEL _cmd, NSString *str) {
        NSLog(@"%@", str);
    }
    // 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        // [NSStringFromSelector(sel) isEqualToString:@"run"];
        if (sel == NSSelectorFromString(@"eat:")) {
            BOOL isSuccess = class_addMethod(self, sel, (IMP)addEat, "v@:@");
            return isSuccess;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    • class_addMethod参数解释(可以command+shift+0查看官方文档)
      class_addMethod(Class cls, SEL name, IMP imp,const char *types)
    1. class: 给哪个类添加方法
    2. SEL: 添加方法的方法编号
    3. IMP: 方法实现 (添加方法的函数实现(函数地址))
    4. type: 方法类型,(返回值+参数类型)
      (1) v 返回值类型是void
      (2)@ 对象->self
      (3): 表示SEL->_cmd
      (4)@ 第四个参数
    • resolveInstanceMethod的作用
      当调用了没有实现的方法没有实现就会调用,然后就可以根据他的参数sel(参数sel就是没有实现的方法)来做一系列的操作。

    4.给分类添加属性
    在分类中,所写的@property (nonatomic, strong) NSString *name;都仅仅是生成了get和set方法,并没有生成对应的_name属性,但是有时候我们会有一种需求,想要让分类中保存一下新的属性值,因为set和get方法只能是对已经有的东西做操作,比如说最常用的UIView的分类我们对frame中的x,y,width,height做操作。

    //给Person添加一个分类addProperty
    //在Person+addProperty.h中
    @property (nonatomic, strong) NSString *name;
    //在Person+addProperty.m中
    #import <objc/message.h>
    @implementation Person (addProperty)
    - (void)setName:(NSString *)name{
        objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (NSString *)name{
        return objc_getAssociatedObject(self, @"name"); 
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //给分类动态添加属性
        Person * p1 = [[Person alloc] init];
        p1.name = @"这是给分类添加的属性";
        NSLog(@"%@",p1.name);
    }
    

    解释:
    objc_setAssociatedObject方法

    /**
         *  根据某个对象,还有key,还有对应的策略(copy,strong等) 动态的将值设置到这个对象的key上
         *  @param object 某个对象
         *  @param key    属性名,根据key去获取关联的对象
         *  @param value  要设置的值
         *  @param policy 策略(copy,strong,assign等)
         */
        OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
    

    objc_getAssociatedObject方法

    /**
         *  根据某个对象,还有key 动态的获取到这个对象的key对应的属性的值
         *  @param object 某个对象
         *  @param key    key
         *  @return 对象的值
         */
        OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
    

    4.实现字典转模型的自动转换
    字典转模型KVC实现会有很多弊端,利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
    1.当字典的key和模型的属性匹配不上。
    2.模型中嵌套模型(模型属性是另外一个模型对象)。
    3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
    注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解;

    步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
    MJExtension 字典转模型实现,底层也是对 runtime 的封装。

    注:本文参考 http://www.jianshu.com/p/19f280afcb24
    更全面的例子参考 https://github.com/lizelu/ObjCRuntimeDemo

    相关文章

      网友评论

          本文标题:Runtime简介

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