iOS-RunTime介绍及使用

作者: 劉光軍_MVP | 来源:发表于2017-11-01 23:00 被阅读215次

    一、RunTime概念

    RunTime简称运行时,我们总是听说OC是动态语言运行时机制,也就是系统在运行时候的一些机制,其中最重要的是消息机制。C语言,函数的调用在编译的时候会决定调用哪个函数,如果调用未实现的函数就会报错,而OC语言属于动态调用过程,在编译时并不能决定真正调用哪个函数,只有在真正的运行的时候才会根据函数的名称找到对应函数来调用,当调用该对象上某个方法,而该对象上没有实现这个方法的时候,可以通过“消息转发”进行解决,也就是说,在编译截断,OC可以调用任何函数,即使是这个函数没有实现,只要声明过就不会报错。

    二、OC调用方法在RunTime中的具体实现

    《一》RunTime消息机制

    消息机制是运行时里面最重要的机制,OC是动态语言,本质都是发送消息,每个方法在运行时会被动态转化为消息发送,即:objc_msgSend(receiver, selector)
    比如:

    • OC代码实例方法调用底层的实现:
    BackView *backView = [[BackView alloc] init];
    [backView changeBgColor];
    
    //编译时底层转化
    //objc对象的isa指针指向他的类对象,从而可以找到对象上的方法
    //SEL:方法编号,根据方法编号就可以找到对应方法的实现。
    [backView performSelector:@selector(changeBgColor)];
    //performSelector本质即为运行时,发送消息,谁做事情就调用谁 
    objc_msgSend(backView, @selector(changeBgColor));
    // 带参数
    objc_msgSend(backView, @selector(changeBgColor:),[UIColor RedColor]);
    
    • OC代码类方法调用底层的实现
    //本质是将类名转化成类对象,初始化方法其实是创建类对象。
    [BackView changeBgColor];
    //BackView 只是表示一个类名,调用方法其实是用的类对象去调用的。(类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。)
    //编译时底层转化
    
    //RunTime 调用类方法同样,类方法也是类对象去调用,所以需要获取类对象,然后使用类对象去调用方法
    Class backViewClass = [BackView class];
    [backViewClass performSelector:@selector(changeBgColor)];
    //performSelector本质即为运行时,发送消息,谁做事情就调用谁 
    
    //类对象发送消息
    objc_msgSend(backViewClass, @selector(changeBgColor));
    // 带参数
    objc_msgSend(backViewClass, @selector(changeBgColor:),[UIColor RedColor]);
    

    selector(SEL):是一个SEL方法选择器。
    SEL其主要作用是快速的通过SEL其主要作用是快速的通过方法名字查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的地址,地址中存放着方法的名字。
    对于一个类中。每一个方法对应着一个SEL。所以一个类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

    • 消息传递的底层实现
      这里我们要先说一下,一个Objc对象如何进行内存布局的,我们先看一下objc_class源码:
    // runtime.h(类在runtime中的定义)
    
    struct objc_class {
      Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
      #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; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
      struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
      #endif
      } OBJC2_UNAVAILABLE;
    

    objc对象内存布局:

    <1>所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中。
    <2>每个对象内部都有一个isa指针,指向它的类对象,类对象中存放着本对象的:
        1、对象方法列表(对象能够接受的消息列表,保存再它所对应的类对象中)
        2、成员变量的列表
        3、属性列表
    

    每一个类都有一个方法列表Method List,保存着类里面所有的方法,根据SEL传入的方法编号找到方法,然后找到方法的实现,然后在方法的实现里面实现。

    • 消息发送动态查找对应的方法
      <1>实例对象调用方法后,底层调用[objc performSelector:@selector(SEL)];方法,编译器将代码转化为objc_msgSend(receiver, selector)
      <2>objc_msgSend函数中,首先通过objcisa指针找到objc对应的class,在class中先去cache中通过SEL查找对应函数的 method,如果找到则通过 method中的函数指针跳转到对应的函数中去执行。
      <3>如果在cacha中未找到,再去methodList中查找,如果能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
      <4>如果在methodlist中未找到,则去superClass中去查找,如果能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

    • 消息传递的过程
      <接上↑>objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,即:objc_msgSend(receiver, selector)。如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
      <1> Method resolution
      objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:实例方法和类方法),让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。
      <2> Message Forwarding

      • <1>Fast forwarding
        如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
      • <2>Normal forwarding
        这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

    《二》使用RunTime动态的添加对象的成员变量和方法

    • 动态添加方法
      动态给某各类添加方法,相当于懒加载机制。这里我们以实例方法为例,首先我们先不实现对象方法,当调用performSelector:方法的时候,再去动态加载方法调用。[bg performSelector:@selector(changeBgColor)];当编译时是不会报错的,运行时才会报错,因为这里我们BaseView类中并没有实现changeBgColor这个方法,当去类的Method List中发现找不到changeBgColor方法,会报错找不到这个方法。这里我们就用到了上面提到的消息转发机制。当调用了没有实现的对象方法时,就会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法,当调用了没有实现的类方法的时候,就会调用+(BOOL)resolveClassMethod:(SEL)sel方法。所以通过这两个方法就可以动态添加方法,参数sel即表示没有实现的方法。一个objective - C方法最终都是一个C函数,默认任何一个方法都有两个参数。self : 方法调用者 _cmd : 调用方法编号。我们可以使用函数class_addMethod为类添加一个方法以及实现。
      动态添加方法:
    +(BOOL)resolveInstanceMethod:(SEL)sel
    {
        // 动态添加changeBgColor方法
        // 首先判断sel是不是changeBgColor方法 也可以转化成字符串进行比较。    
        if (sel == @selector(changeBgColor)) {
        /** 
         第一个参数: cls:给哪个类添加方法
         第二个参数: SEL name:添加方法的编号
         第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
         第四个参数: types :方法类型,需要用特定符号,参考API
         */
          class_addMethod(self, sel, (IMP) newChangeBgColor , "v@:");
            // 处理完返回YES
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void newChangeBgColor(id self ,SEL _cmd)
    {
    
    }
    

    types:方法类型表:


    type方法类型表.png
    • 动态添加变量
      1-> 动态获取类中的所有属性(包括私有)
    Ivar *ivar = class_copyIvarList([self.baseView class], &count);
    

    2->遍历属性找到对应属性字段

    const char *varName = ivar_getName(var);
    

    3->修改对应的字段

    object_setIvar(self.baseView, var, @"newName");
    

    具体:

    -(void)addNewName{
        unsigned int count = 0;
        Ivar *ivar = class_copyIvarList([baseView class], &count);
        for (int i = 0; i<count; i++) {
            Ivar var = ivar[i];
            const char *varName = ivar_getName(var);
            NSString *name = [NSString stringWithUTF8String:varName];
    
            if ([name isEqualToString:@"oldName"]) {
                object_setIvar(baseView, var, @"newName");
                break;
            }
        }
      
        self.nameLabel.text = baseView.oldName;
    }
    

    《三》动态交换方法

    当遇到所使用的系统方法或者不可修改的静态库方法功能不够时,需要给此类方法扩展一些功能。比如我们有个BaseView类中有个changeBgColor的方法,此时我们想在这个方法里做些操作,我们定义一个newChangeBgColor的方法。因为交换只需进行一次,所以我们在BaseView的Categary中的load方法中,当加载分类的时候交换方法即可。交换方法的本质其实是交换两个方法的实现
    即:
    1根据SEL方法编号在Method List中找到方法
    2交换两个IMP指针指向的方法实现

    交换方法内部实现.png
    +(void)load
    {
        // 获取要交换的两个方法
        // 获取类方法  用Method 接受一下
        // class :获取哪个类方法 
        // SEL :获取方法编号,根据SEL就能去对应的类找方法。
        Method oldChangeColorMethod = class_getClassMethod([UIImage class], @selector(changeBgColor));
        // 获取第二个类方法
        Method newChangeColorMethod = class_getClassMethod([UIImage class], @selector(newChangeBgColor));
        // 交换两个方法的实现 方法一 ,方法二。
        method_exchangeImplementations(oldChangeColorMethod, newChangeColorMethod);
        // IMP其实就是 implementation的缩写:表示方法实现。
    }
    

    注意:交换方法的时候newMethod里就不能再调用oldMethod方法了,因为调用oldMethod方法实质上相当于调用newMethod方法,会循环引用造成死循环。

    《四》RunTim动态添加属性

    XCode运行在Category的.h文件声明@property编译通过,但运行时如果没有runtime处理,进行赋值取值,就会报错。

    • @property的本质是什么
      @property = ivar + getter + setter;
      说人话:

    “属性”(property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。

    “属性”(property)作为OC的一项特性,主要的作用就在于封装对象总的数据。OC 对象通常会把其所需要的数据保存为各种实例变量,实例变量一般通过“存取方法”(access method)来访问,其中,“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。在正规的OC编码风格中,存取方法有着严格的命名规范,正因为有了这种严格的命名规范,所以OC可以根据名称自动创建出存取方法,其实也可以把属性当做一种关键字,可以表示:

    编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量,所以你也可以这么说:@property=getter+setter;
    比如下面的这个类:

    @interface Person : NSObject
    @property NSString *firstName;
    @property NSString *lastName;
    @end
    

    上述代码写出来的类与下面这种写法等效:

    @interface Person : NSObject
    - (NSString *)firstName;
    - (void)setFirstName:(NSString *)firstName;
    - (NSString *)lastName;
    - (void)setLastName:(NSString *)lastName;
    @end
    

    而objc_property是一个结构体,包括name和attributes,定义如下:

    struct property_t {
        const char *name;
        const char *attributes;
    };
    

    而attributes本质是objc_property_attribute_t,定义了property的一些属性,定义如下:

    /// Defines a property attribute
    typedef struct {
        const char *name;           /**< The name of the attribute */
        const char *value;          /**< The value of the attribute (usually empty) */
    } objc_property_attribute_t;
    

    而attributes的具体内容是什么呢?其实,包括:类型,原子性,内存语义和对应的实例变量。
    例如:我们定义一个string的property@property (nonatomic, copy) NSString *string;
    ,通过 property_getAttributes(property)
    获取到attributes并打印出来之后的结果为T@"NSString",C,N,V_string

    其中T就代表类型,可参阅Type Encodings,C就代表Copy,N代表nonatomic,V就代表对于的实例变量。
    ivar、getter、setter 是如何生成并添加到这个类中的?

    “自动合成”( autosynthesis)
    完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.

    @implementation Person
    @synthesize firstName = _myFirstName;
    @synthesize lastName = _myLastName;
    @end
    

    属性是怎么实现的呢?

    1、OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
    2、setter 与 getter 方法对应的实现函数
    3、ivar_list :成员变量列表
    4、method_list :方法列表
    5、prop_list :属性列表
    也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.
    如何在@protocol和category中使用@property?
    1、在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性

    2、category 使用 @property 也是只会生成 setter 和 getter 方法的声明,但是不会自动生成私有属性,如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:

    1、动态添加属性
    objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
    参数二:void * == id  key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
    参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
    参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
    >>>>>
     typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
     OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
     OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
     OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
     OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
     OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
    };
    
    获得属性
    objc_getAssociatedObject(id object, const void *key);
    参数一:id object : 获取哪个对象里面的关联的属性。
    参数二:void * == id  key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
    此时已经成功给NSObject添加name属性,并且NSObject对象可以通过点语法为属性赋值。
    

    下面这个也是一样的:

    -(void)setName:(NSString *)name
    {
        objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    -(NSString *)name
    {
        return objc_getAssociatedObject(self, @"name");    
    }
    
    《RunTime字典转模型》

    通过给NSObject添加分类,声明并实现使用Runtime字典转模型的类方法:

    + (instancetype)modelWithDict:(NSDictionary *)dict
    

    KVC字典转模型和RunTime转模型的区别:

    KVC:KVC字典转模型实现原理是遍历字典中所有Key,然后去模型中查找相对应的属性名,要求属性名与Key必须一一对应,字典中所有key必须在模型中存在。
    RunTime:RunTime字典转模型实现原理是遍历模型中的所有属性名,然后去字典查找相对应的Key,也就是以模型为准,模型中有哪些属性,就去字典中找那些属性。

    RunTime字典转模型的优点:当服务器返回的数据过多,而我们只使用其中很少一部分时,没有用的属性就没有必要定义成属性浪费不必要的资源。只保存最有用的属性即可。

    字典转模型简要过程:
    1、创建模型对象
    2、使用class_copyIvarList方法copy成员属性列表

    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    

    参数一:__unsafe_unretained Class cls : 获取哪个类的成员属性列表。这里是self,因为谁调用分类中类方法,谁就是self。
    参数二:unsigned int *outCount : 无符号int型指针,这里创建unsigned int型count,&count就是他的地址,保证在方法中可以拿到count的地址为count赋值。传出来的值为成员属性总数。
    返回值:Ivar * : 返回的是一个Ivar类型的指针 。指针默认指向的是数组的第0个元素,指针+1会向高地址移动一个Ivar单位的字节,也就是指向第一个元素。Ivar表示成员属性。
    3、遍历成员属性列表,获得属性列表

    for (int i = 0 ; i < count; i++) {
         // 获取成员属性
         Ivar ivar = ivarList[i];
    }
    

    4、使用ivar_getName(ivar)获得成员属性名,因为成员属性名返回的是C语言字符串,将其转化成OC 字符串

    NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
    或者ivar_getTypeEncoding(ivar)方法
    

    5、获得的成员属性名是带的成员属性,去掉,获得属性名,也就是字典的key。

    // 获取key
    NSString *key = [propertyName substringFromIndex:1];
    

    6、获取字典中key对应的Value。

    id value = dict[key];
    

    7、给模型属性赋值,并将模型返回

    if (value) {
    // KVC赋值:不能传空
    [objc setValue:value forKey:key];
    }
    return objc;
    

    二级模型转化方法:

    + (instancetype)modelWithDict:(NSDictionary *)dict{
        // 1.创建对应类的对象
        id objc = [[self alloc] init];
        // count:成员属性总数
        unsigned int count = 0;
       // 获得成员属性列表和成员属性数量
        Ivar *ivarList = class_copyIvarList(self, &count);
        for (int i = 0 ; i < count; i++) {
            // 获取成员属性
            Ivar ivar = ivarList[i];
            // 获取成员名
           NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
            // 获取key
            NSString *key = [propertyName substringFromIndex:1];
            // 获取字典的value key:属性名 value:字典的值
            id value = dict[key];
            // 获取成员属性类型
            NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            // 二级转换
            // value值是字典并且成员属性的类型不是字典,才需要转换成模型
            if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
                // 进行二级转换
                // 获取二级模型类型进行字符串截取,转换为类名
                NSRange range = [propertyType rangeOfString:@"\""];
                propertyType = [propertyType substringFromIndex:range.location + range.length];
                range = [propertyType rangeOfString:@"\""];
                propertyType = [propertyType substringToIndex:range.location];
                // 获取需要转换类的类对象
               Class modelClass =  NSClassFromString(propertyType);
               // 如果类名不为空则进行二级转换
                if (modelClass) {
                    // 返回二级模型赋值给value
                    value =  [modelClass modelWithDict:value];
                }
            }
            if (value) {
                // KVC赋值:不能传空
                [objc setValue:value forKey:key];
            }
        }
        // 返回模型
        return objc;
    }
    

    总结

    上述对RunTime的总结只是一些自己平时的积累,借鉴了一些好的博文资料加上自己的一些理解,还有很多东西没有理解到位,还请多多指教。

    相关文章

      网友评论

        本文标题:iOS-RunTime介绍及使用

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