runtime从概念到应用

作者: 堂吉诃德灬 | 来源:发表于2016-08-20 16:00 被阅读200次

    以前也看过好多关于runtime的文章,可是总觉得一知半解,对其真正怎么应用一直无法理解,这段时间趁着闲暇,自己又翻看了几篇博客,在这里把这里把自己的心得体会跟大家分享,在这里不敢保证对runtime有了很深的理解,但是最起码不会像原先一直是云遮雾绕的。

    1. 运行时简介

    OC是一门动态的语言,它将很多静态语言在编译和链接时期做的事方到了运行时来处理。在runtime中,对象可以用C语言中的结构体来表示,方法可以用C语言中的函数来实现,这些结构体和函数被runtime函数封装后,让OC面向对象变为了可能。

    2. 类和对象

    当我们学习OC语言的时候我们首先要知道两大基础概念,分别是类和对象,类是一个抽象的概念,对象则是类的实体。在OC中类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针,下面我们来看下它的结构

    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY; 
    #if !__OBJC2__
        Class super_class                                 OBJC2_UNAVAILABLE;       //父类指针 
        const char *nam                                   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;  //方法缓存    
        struct objc_protocol_list*protocols               OBJC2_UNAVAILABLE; //类的协议链表  
    #endif
    }OBJC2_UNAVAILABLE;
    

    父类的指针建立了类的层次机构,类的属性、方法、协议和成员变量定义了类能做什么。但是需要注意的是属性、协议和方法都存储在类的可写段中,这些信息可以在运行时被改变,这也是分类的实现原理,但是类的成员变量存储在只读段中,不能被修改,这也是分类不能添加成员变量的原因。

    对象是类的一个实例,对象用objc_object来表示

    struct objc_object {
            Class isa  OBJC_ISA_AVAILABILITY;  //isa指针指向其类
    };
    

    上面分别是类和对象在OC中的具体体现,当我们向一个类的实体对象发送消息时,实体对象会根据其内部的isa指针,寻找其对应的类,找到对应的类之后,会先在类的方法缓存中寻找对应的方法,找到执行,没找到上其自身内部的方法链表去寻找,还没找到,根据其内部的父类上父类去寻找,以此类推。

    3. 元类(meta-class)

    上面讲述的是当一个对象调用方法的时候在OC内部的处理过程,但是我们知道类本身也是可以调用方法的即是类方法,那么类其实也可以看做一个对象,那么当它调用类方法的时候在OC内部是如何处理的呢?在上面类的结构体中我们可以看到在类中有一个isa指针,这个isa指针其实指向的是元类,元类是一个包含这个类的类方法的结构体,它存储了这个类的所有类方法,所以当我们调用类方法的时候其实就是在类本身的isa指针指向的元类里面寻找。

    元类(meta-class)也是一个类,也可以向它发送消息,它也有isa指针,为了不让这种结构无限延展下去,OC的设计者让所有的元类的isa指针指向了根类的元类,而根类的元类的指针又指向了它本身。

    总结:对象里面有一个isa指针,这个指针指向了它所属的类,而在类里面也有一个isa指针,这个指针指向元类,元类是一个存储了所有类方法的结构体,在元类里面也有一个isa指针,这个指针指向根类的元类,而根类的元类的isa指针指向根类自己。

    4. runtime中的相关定义

    class:定义OC类
    Ivar:定义对象的实例变量,包括类型和名字
    Protocol:定义正式协议
    objc_property_t:定义属性
    Method:定义对象方法或类方法,这个类型提供了方法的名字(就是选择器)、参数数量和类型以及返回值(这些信息合起来称为方法的签名),还有有一个指向代码的函数指针(也就是方法的实现IMP)
    SEL:定义选择器,选择器是方法名的唯一标识符
    IMP:定义方法的实现,这只是一个指向某个函数的指针

    5. 方法与消息的相关定义

    SEL

    SEL:选择器,是表示一个方法的selector指针,其定义如下:

    typedef struct objc_selector *SEL;
    

    OC在编译时会根据每一个方法的名字,参数序列,生成一个唯一的整型标识,这个表示就是SEL,SEL代表着方法的签名,每一个方法都对应着一个SEL,所以一个类中不能定义2个同名的方法,因为只要方法名相同,它们的SEL就是一样的,这样在根据SEL寻找对应的方法的实现的时候就会出错。所以当一个对象调用方法的时候其实就是根据该方法的SEL到该类的方法列表去寻找该方法的实现

    SEL的实质是根据方法名hash化了的一个字符串,这个字符串指向了方法的实现的地址,而对于字符串只需要比较它们的地址就可以了,SEL的存在是为了加快方法的查询速度。通过下面三种方法可以获取SEL:
    1 sel_registerName函数
    2 OC编译器提供的@selector()
    3 NSSelectorFromString()

    IMP

    IMP实际上是一个函数指针,指向方法的实现,定义如下:

    id (*IMP)(id, SEL,...)
    

    第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
    第二个参数:是方法选择器(selector)
    接下来的参数:方法的参数列表。
    每一个方法都对应一个SEL,而SEL就是为了查找方法的最终实现IMP的

    Method

    Method的定义如下:

    typedefstruct objc_method *Method
    structobjc_method{
        SEL method_name      OBJC2_UNAVAILABLE; // 方法名
        char *method_types   OBJC2_UNAVAILABLE;
        IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
    }
    

    通过上面可见在方法的结构体里面包含了一个SEL和IMP,所以相当于在SEL和IMP之间做了一个映射,当我们调用方法的时候,首先会根据方法名寻找其对应的SEL,然后根据SEL来寻找方法对应的实现IMP,但是为了更快的寻找到方法的实现,OC在类的结构体中加入了一个方法的缓存,寻找时会先在方法缓存中寻找,然后再在其方法链表中寻找,如果找不到,上父类寻找,直到找到根类还寻找不到就会报错

    6. 方法的调用

    在OC中,消息直到运行时才绑定到方法的实现上。编译器会将消息表达式[receive message]转化为一个消息函数的调用,即objc_msgSend,这个函数将消息接受者和方法名作为其基础参数,如下所示:

    objc_msgSend(receiver, selector)
    

    如果消息中还有其他参数,则该方法的形式如下:

    objc_msgSend(receiver,selector,arg1,arg2,…)
    

    首先它找到selector对应的方法实现IMP,因为同一个方法可能在不同的类中有不同的实现,所以我们要依赖于接受者的类来找到确切的实现
    调用方法实现,将接受者对象及方法的所有参数都传给它
    最后,它将实现返回的值作为它自己的返回值

    具体方法的调用流程:
    如果用实例对象调用方法会首先根据对象的isa指针找到对象所属的类,然后会首先在类的方法缓存列表中寻找,如果找到,直接执行,如果没找到从类的方法链表里面寻找,找到,执行,没找到上父类寻找,如果找到根类还没找到,就会转向拦截调用,如果没有重写拦截调用方法,程序就会报错。

    如果调用的是类方法,先从类的方法缓存列表中寻找,如果找到,直接执行,如果没找到从类的方法链表里面寻找,找到,执行,没找到上父类寻找,如果找到根类还没找到,就会转向拦截调用,如果没有重写拦截调用方法,程序就会报错。

    所以当我们重写父类的方法的时候,其实并没有覆盖掉父类的方法,只是当在当前类找到后就不再去父类寻找,如果想要调用父类的方法,那么需要调用Super,它会在运行时跳过在当前类寻找,直接转向父类寻找。

    7. 拦截调用

    拦截调用是当找不到调用的方法导致在程序崩溃之前,我们有机会重写NSObject的四个方法来处理

    +(BOOL)resolveClassMethod:(SEL)sel;//类方法
    +(BOOL)resolveInstanceMethod:(SEL)sel;//实例方法
    //后两个方法需要转发到其他的类处理
    -(id)forwardingTargetForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    

    第一个方法是当调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,可以加上自己的处理然后返回YES

    第二个方法和第一个方法相似,只不过处理的是实例方法

    第三个方法是将调用的不存在的方法重定向到一个声明了这个方法的类,只需要返回一个有这个方法的target

    第四个方法是将你调用的不存在的方法打包成NSInvocation传给你,做完自己的处理后,调用invokeWithTarget方法让某个target触发这个方法

    8. NSInvocation

    NSInvocation把一个目标,一个选择器,一个方法签名和所有的参数都塞进一个对象里,这个对象可以先存储起来,以备将来调用。当NSInvocation被调用时,它会发送信息,OC运行时会找到正确的方法实现来执行。

    NSInvocation包含一个目标和一个选择器。目标是一个接收消息的对象,选择器则是被发送的对象,NSInvocation还包含一个方法签名(NSMethodSignature),它封装了一个方法的返回类型和参数类型。一个NSMethodSignature不包含方法名称,只有返回类型和参数

    9. 动态添加方法

    当从外部调用一个不存在的方法的时候:

    [target performSelector:@selector(resolveAdd:)withObject:@"test"];
    

    如果调用的是类方法,那么需要重写resolveClassMethod,如果调用的是静态方法,则需要重写resolveInstanceMethod方法,这里以重写resolveInstanceMethod为例

    //这一块其实就是方法拦截的第一步了
    void runAddMethod(id self, SEL _cmd, NSString *string){
        NSLog(@"这是不存在的方法的实现", string);
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        //给本类动态添加一个方法
        if ([NSStringFromSelector(sel)isEqualToString:@"resolveAdd:"]) {
            class_addMethod(self, sel,(IMP)runAddMethod, "v@:*");
     return YES;
        }
    return NO ;
       
    }
    
    //在这里一定要注意runAddMethod这个方法不是我们平常惯用的OC方法,而是另一个IMP类型的方法,它后面最基本是有两个参数的,即前两个。上面的v@:*"代表的意思是什么呢?V代表的是返回的是void,@代表的self,:代表的sel,*代表的参数,注意参数有几个,*就有几个
    

    其中class_addMethod的四个参数分别是:
    1.Class class 给哪个类添加方法,本例中是self
    2.SEL name 添加的方法,本例中是重写的拦截调用传进来的selector。
    3.IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
    4."V@:*"方法的签名,它代表一个有参数的方法,没参数为"V@:"

    如果resolve方法返回NO,运行时就会移到下一步:消息转发(message forwading)

    如果目标对象实现了 -forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会

    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if(aSelector ==@selector(foo:)){
            AnOtherObject  *another = [[AnOtherObject alloc]init];
            if(([stu respondsToSelector: aSelector])
           {
                 return  another;
           }
         }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    只要这个方法返回的不是nil和self,整个消息发送的过程会被重启,发送的对象会变成你返回的那个对象,只要被转发的消息实现了这个方法,程序就可以运行。

    如果以上都不正确,runtime会先发送-methodSignatureForSelector:消息获得函数的参数和返回值类型(即获得方法签名),如果methodSignatureForSelector返回nil,runtime则会发出doseNotRecognizeSelector:消息,程序这时也就挂掉了,如果返回了一个函数签名,runtime就会创建一个NSInvocation对象并发送-forwardingInvocation:消息给目标对象

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSString *method = NSStringFromSelector(aSelector);
        if ([method isEqualToString:@"eat"]) {
            NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    //这里因为当前对象没有实现这个方法,所以获取不到方法签名,如果我们不创建方法签名程序就会崩溃,所以我们自己创造一个方法签名
            return signature;
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    //当有方法签名返回的时候就会调用这个方法,方法签名里面包含了两个重要的属性,分别是selector和target,然后我们可以调用invokeWithTarget让另一个对象去执行这个方法。
    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {
        SEL method = [anInvocation selector];
        Student *stu = [[Student alloc]init];
        if ([stu respondsToSelector:method]) {
            [anInvocation invokeWithTarget:stu];
        }
    }
    

    消息的发送流程:
    1.当发送一个消息时会先尝试找到该消息,如果找到了,跳到相应的函数IMP去执行实现代码;
    2.如果没有找到,runtime会发送+resolveInstanceMeyhod:或者+resolveClassMethod:尝试去resolve这个消息;
    3.如果resolve方法返回NO,runtime就发送-forwardingTargetForSelector:允许你把这个消息转发给另一个对象;
    4.如果没有新的目标对象返回,runtime就会发送-methodSignatureForSelector和-forwardInvocation消息,你可以发送-invokeWithTarget消息来手动转发消息或者发送doseNotRecognizeSelector抛出异常

    10. 方法交换

    10.1 交换两个方法的实现

    需要用到的方法<objc/runtime.h>
    获得某个类的类方法

    Method class_getClassMethod(Class cls , SEL name)
    

    获得某个类的实例对象方法

    Method class_getInstanceMethod(Class cls , SEL name)
    

    交换两个方法的实现

    void method_exchangeImplementations(Method m1 , Method m2)
    

    具体例子:

     Method m1 = class_getInstanceMethod([self class], @selector(toGetName));
     Method m2 =class_getInstanceMethod([self class], @selector(toGetAge));
     method_exchangeImplementations(m1, m2);
    
    10.2 拦截系统方法

    1、为UIImage建一个分类(UIImage+Category)
    2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断

    + (UIImage *)xh_imageNamed:(NSString *)name{ 
        double version = [[UIDevicecurrentDevice].systemVersion doubleValue];  
      if (version >= 7.0) {        // 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片        
     name = [name stringByAppendingString:@"_os7"];     
    }  
      return [UIImage xh_imageNamed:name];
     }
    

    3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

    + (void)load {    // 获取两个类的类方法    
     Method m1 = class_getClassMethod([UIImageclass], @selector(imageNamed:));   
     Method m2 = class_getClassMethod([UIImage class],@selector(xh_imageNamed:));    
     // 开始交换方法实现 
        method_exchangeImplementations(m1, m2);
     }
    

    11. 关联引用

    在分类中是无法添加属性的,但是我们可以利用runtime来为分类添加属性

    void objc_setAssociatedObject(id object , const void *key,id value ,objc_AssociationPolicy policy)
    

    参数 object:给哪个对象设置属性参数
    key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节(注意这个key的地址要是唯一的所以要用static声明)
    参数value:给属性设置的值
    参数policy:存储策略 (assign 、copy 、retain就是strong)

    id objc_getAssociatedObject(id object , const void *key)
    

    利用参数key将对象object中存储的值取出来

    12. 获取类的成员变量、属性、方法

    unsigned int Count;
        objc_property_t *propertyList =class_copyPropertyList([self class], &Count); //获取类的属性列表
       for (int i = 0; i < Count; i++) {
            const char *name =property_getName(propertyList[i]);//获取属性的名字
            NSLog(@"属性名为%@",[NSStringstringWithUTF8String:name]);
    }
    free(propertyList);
    
    unsigned int count;
        Ivar *ivar =class_copyIvarList([self class], &count);//获取成员变量链表
        for (int i = 0; i < count;i++) {
            const char *name =ivar_getName(ivar[i]);//获取成员变量名字
           NSLog(@"实体变量名为%@",[NSStringstringWithUTF8String:name]);
       }
    free(ivar);
    
    unsigned int Count;
        Method *methodList =class_copyMethodList([self class], &Count);//获取类的方法列表
        for (int i = 0; i < Count;i++) {
            Method method =methodList[i];//获取具体方法
            SEL sel =method_getName(method);//转为sel
            NSLog(@"方法名为%@",NSStringFromSelector(sel));//根据sel获取方法名字
      }
    free(methodList);
    

    注:如果方法只是在.h声明了,但是在.m并没有实现,那么获取方法列表时是获取不到那个方法的

    相关文章

      网友评论

      本文标题:runtime从概念到应用

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