zoukankan      html  css  js  c++  java
  • iOS Runtime原理及使用

     

    runtime简介

    因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

    RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

    Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

    Runtime相关的头文件

    ios的sdk中 usr/include/objc文件夹下面有这样几个文件

    都是和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。 在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。

    使用时,需要导入文件,导入如:

    #import <objc/message.h>
    #import <objc/runtime.h>

    runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。 主要包括:

    操作对象的类型的定义

    复制代码
    /// An opaque type that represents a method in a class definition. 一个类型,代表着类定义中的一个方法
    typedef struct objc_method *Method;
    
    /// An opaque type that represents an instance variable.代表实例(对象)的变量
    typedef struct objc_ivar *Ivar;
    
    /// An opaque type that represents a category.代表一个分类
    typedef struct objc_category *Category;
    
    /// An opaque type that represents an Objective-C declared property.代表OC声明的属性
    typedef struct objc_property *objc_property_t;
    
    // Class代表一个类,它在objc.h中这样定义的  typedef struct objc_class *Class;
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    
    #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;
    复制代码

    这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,对操作一个类属性和方法非常方便。OBJC2_UNAVAILABLE标记的属性是Ojective-C 2.0不支持的,但实际上可以用响应的函数获取这些属性,例如:如果想要获取Class的name属性,可以按如下方法获取:

    Class classPerson = Person.class;
    // printf("%s
    ", classPerson->name); //用这种方法已经不能获取name了 因为OBJC2_UNAVAILABLE
    const char *cname  = class_getName(classPerson);
    printf("%s", cname); // 输出:Person

    函数的定义

    • 对对象进行操作的方法一般以object_开头
    • 对类进行操作的方法一般以class_开头
    • 对类或对象的方法进行操作的方法一般以method_开头
    • 对成员变量进行操作的方法一般以ivar_开头
    • 对属性进行操作的方法一般以property_开头开头
    • 对协议进行操作的方法一般以protocol_开头

    根据以上的函数的前缀 可以大致了解到层级关系。

    对于以objc_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息,类的列表,关联对象和关联属性等操作。

    例如:使用runtime对当前的应用中加载的类进行打印,别被吓一跳。

    复制代码
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        unsigned int count = 0;
        Class *classes = objc_copyClassList(&count);
        for (int i = 0; i < count; i++) {
            const char *cname = class_getName(classes[i]);
            printf("%s
    ", cname);
        }
    }
    复制代码

    runtime应用

    发送消息

    方法调用的本质,就是让对象发送消息。

    objc_msgSend,只有对象才能发送消息,因此以objc开头.

    使用消息机制前提,必须导入#import <objc/message.h>

    消息机制简单使用:

    复制代码
         // 创建person对象
        Person *p = [[Person alloc] init];
    
        // 调用对象方法
        [p eat];
    
        // 本质:让对象发送消息
        objc_msgSend(p, @selector(eat));
    
        // 调用类方法的方式:两种
        // 第一种通过类名调用
        [Person eat];
        // 第二种通过类对象调用
        [[Person class] eat];
    
        // 用类名调用类方法,底层会自动把类名转换成类对象调用
        // 本质:让类对象发送消息
        objc_msgSend([Person class], @selector(eat));
    复制代码

    我们可以通过clang来查看代码生成的CPP代码。

    最终代码,需要把当前代码重新编译,用xcode编译器,clang

    clang -rewrite-objc main.m 查看最终生成代码

    交换方法

    交换方法实现的需求场景:自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。

    可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码 的情况下,就完成了项目的改进。

    交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。

    复制代码
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        // 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
        // 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
        // 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
        UIImage *image = [UIImage imageNamed:@"123"];
    }
    
    @end
    
    @implementation UIImage (Image)
    // 加载分类到内存的时候调用
    + (void)load
    {
        // 交换方法
    
        // 获取imageWithName方法地址
        Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
    
        // 获取imageWithName方法地址
        Method imageName = class_getClassMethod(self, @selector(imageNamed:));
    
        // 交换方法地址,相当于交换实现方式
        method_exchangeImplementations(imageWithName, imageName);
    }
    
    // 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
    
    // 既能加载图片又能打印
    + (instancetype)imageWithName:(NSString *)name
    {
        // 这里调用imageWithName,相当于调用imageName
        UIImage *image = [self imageWithName:name];
    
        if (image == nil) {
            NSLog(@"加载空的图片");
        }
    
        return image;
    }
    
    @end
    复制代码

    类对象的关联对象

    关联对象不是为类对象添加属性或者成员变量(因为在设置关联后也无法通过ivarList或者propertyList取得) ,而是为类添加一个相关的对象,通常用于存储类信息,例如存储类的属性列表数组,为将来字典转模型的方便。 

    使用方式一:给分类添加属性

    复制代码
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
        // 给系统NSObject类动态添加属性name
        NSObject *objc = [[NSObject alloc] init];
        objc.name = @"小码哥";
        NSLog(@"%@",objc.name);
    }
    
    @end
    
    
    // 定义关联的key
    static const char *key = "name";
    
    @implementation NSObject (Property)
    
    - (NSString *)name
    {
        // 根据关联的key,获取关联的值。
        return objc_getAssociatedObject(self, key);
    }
    
    - (void)setName:(NSString *)name
    {
        // 第一个参数:给哪个对象添加关联
        // 第二个参数:关联的key,通过这个key获取
        // 第三个参数:关联的value
        // 第四个参数:关联的策略
        objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    @end
    复制代码

    使用方式二:给对象添加关联对象。

    比如alertView,一般传值,使用的是alertView的tag属性。我们想把更多的参数传给alertView代理:

    复制代码
    /**
     *  删除点击
     *  @param recId        购物车ID
     */
    - (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"确认要删除这个宝贝" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"确定", nil];
        
        // 传递多参数
        objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
        alert.tag = [recId intValue];
        [alert show];
    }
    
    /**
     *  确定删除操作
     */
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
        if (buttonIndex == 1) {
            
            NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id");
            NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id");
            NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag];
        }
    }
    复制代码

    objc_setAssociatedObject方法的参数解释:

    1. 第一个参数id object, 当前对象
    2. 第二个参数const void *key, 关联的key,是c字符串 
    3. 第三个参数id value, 被关联的对象的值 
    4. 第四个参数objc_AssociationPolicy policy关联引用的规则

     动态添加方法

    开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

    经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。

    简单使用:

    复制代码
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
        Person *p = [[Person alloc] init];
    
        // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
        // 动态添加方法就不会报错
        [p performSelector:@selector(eat)];
    }
    
    @end
    
    
    @implementation Person
    // void(*)()
    // 默认方法都有两个隐式参数,
    void eat(id self,SEL sel)
    {
        NSLog(@"%@ %@",self,NSStringFromSelector(sel));
    }
    
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if (sel == @selector(eat)) {
            // 动态添加eat方法
    
            // 第一个参数:给哪个类添加方法
            // 第二个参数:添加方法的方法编号
            // 第三个参数:添加方法的函数实现(函数地址)
            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
            class_addMethod(self, @selector(eat), eat, "v@:");
        }
        return [super resolveInstanceMethod:sel];
    }
    @end
    复制代码

    字典转模型KVC实现

    KVC:把字典中所有值给模型的属性赋值。这个是要求字典中的Key,必须要在模型里能找到相应的值,如果找不到就会报错。基本原理如:

    复制代码
        // KVC原理:
        // 1.遍历字典中所有key,去模型中查找有没有对应的属性
        [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
            
            // 2.去模型中查找有没有对应属性 KVC
            // key:source value:来自即刻笔记
            // [item setValue:@"来自即刻笔记" forKey:@"source"]
            [item setValue:value forKey:key];
        }];
    复制代码

    但是,在实际开发中,从字典中取值,不一定要全部取出来。因此,我们可以通过重写KVC 中的 forUndefinedKey这个方法,就不会进行报错处理。

    复制代码
    // 重写系统方法? 1.想给系统方法添加额外功能 2.不想要系统方法实现
    // 系统找不到就会调用这个方法,报错
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key
    {
        
    }
    复制代码

    另外,我们可以通过runtime的方式去实现。我们把KVC的原理倒过来,通过遍历模型的值,从字典中取值。

    复制代码
    // Ivar:成员变量 以下划线开头
    // Property:属性
    + (instancetype)modelWithDict:(NSDictionary *)dict
    {
        id objc = [[self alloc] init];
        
        // runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
        // 1.获取模型中所有成员变量 key
        // 获取哪个类的成员变量
        // count:成员变量个数
        unsigned int count = 0;
        // 获取成员变量数组
        Ivar *ivarList = class_copyIvarList(self, &count);
        
        // 遍历所有成员变量
        for (int i = 0; i < count; i++) {
            // 获取成员变量
            Ivar ivar = ivarList[i];
            
            // 获取成员变量名字
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
            // 获取成员变量类型
            NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            // @"User" -> User
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@""" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            // 获取key
            NSString *key = [ivarName substringFromIndex:1];
            
            // 去字典中查找对应value
            // key:user  value:NSDictionary
            
            id value = dict[key];
            
            // 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
            // 并且是自定义对象才需要转换
            if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
                // 字典转换成模型 userDict => User模型
                // 转换成哪个模型
    
                // 获取类
                Class modelClass = NSClassFromString(ivarType);
                
                value = [modelClass modelWithDict:value];
            }
            
            // 给模型中属性赋值
            if (value) {
                [objc setValue:value forKey:key];
            }
        }
            
        return objc;
    }
    复制代码

    基本的代码就是上面这样。

    面试题

    说说什么是runtime

    1>OC 是一个全动态语言,OC 的一切都是基于 Runtime 实现的
    平时编写的OC代码, 在程序运行过程中, 其实最终都是转成了runtime的C语言代码, runtime算是OC的幕后工作者
    比如:

    OC :
    [[Person alloc] init]
    runtime :
    objc_msgSend(objc_msgSend("Person" , "alloc"), "init")

    2>runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API
    3>runtimeAPI的实现是用 C++ 开发的(源码中的实现文件都是mm),是一套苹果开源的框架

    使用过runtime吗,用它来做什么

    本文二、三部分。

    参考:

    http://www.cnblogs.com/Mike-zh/p/4557014.html

    http://www.jianshu.com/users/b09c3959ab3b/latest_articles

    runtime的方法调用流程?

         怎么去调用eat方法 ,对象方法:类对象的方法列表 类方法:元类中方法列表

         1.通过isa去对应的类中查找

         2.注册方法编号

         3.根据方法编号去查找对应方法

         4.找到只是最终函数实现地址,根据地址去方法区调用对应函数

  • 相关阅读:
    swift 第十四课 可视化view: @IBDesignable 、@IBInspectable
    swift 第十三课 GCD 的介绍和使用
    swift 第十二课 as 的使用方法
    swift 第十一课 结构体定义model类
    swift 第十课 cocopod 网络请求 Alamofire
    swift 第九课 用tableview 做一个下拉菜单Menu
    swift 第八课 CollectView的 添加 footerView 、headerView
    swift 第七课 xib 约束的优先级
    swift 第六课 scrollview xib 的使用
    swift 第五课 定义model类 和 导航栏隐藏返回标题
  • 原文地址:https://www.cnblogs.com/pioneerMax/p/13973083.html
Copyright © 2011-2022 走看看