Runtime (整理笔记)

作者: 果哥爸 | 来源:发表于2017-01-18 17:55 被阅读215次

    强烈推荐,关于runtime只需要看下一缕殇流化隐半边冰霜的这几篇文章就够。

    作者:一缕殇流化隐半边冰霜
    神经病院Objective-C Runtime住院第二天——消息发送与转发
    神经病院Objective-C Runtime入院第一天——isa和Class
    神经病院Objective-C Runtime出院第三天——如何正确使用Runtime

    **说明:此文是自己的总结笔记,主要参考这几篇文章:
    iOS开发-Runtime详解
    NSHipster里面的这两篇文章 :
    Associated Objects
    Method Swizzling
    **

    树1.jpg

    一.Runtime简介

    • Runtime又叫运行时,是一套底层的C语言API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。
      比如:

        [receiver doSomething];
        底层运行会被编译器转化为:
        objc_msgSend(receiver, @selector(doSomething))
        如果带有参数比如:
        [receiver doSomething:(id)arg...];
        底层运行时会被编译器转化为:
        objc_msgSend(receiver, @selector(doSomething), arg1, arg2, ...)
      

    可能通过以上你看不出它的价值,但是我们需要了解OC是一门动态语言,它会将一些工作放在代码运行时才处理而非在编译的时候,也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们所编写的代码会转换成完整的代码运行。
    因此,编译器是不够,我们还需要一个运行时的系统来处理编译后的代码

    • Runtime 基本是C和汇编写的,可以充分保证动态系统的高效性

    二. Runtime的作用:

    • 获取某个类的所有成员变量
    • 获取某个类的所有属性
    • 获取某个类的所有方法
    • 交换方法实现
    • 动态添加一个成员变量
    • 动态添加一个方法

    三.Runtime的术语的数据结构

    1.SEL

    • 它是selector在objc中的表示(Swift 中是 Selector类)。selector 是方法选择器,其实作用是对方法名进行包装,以便找到对应的方法实现(注意:Objc 在相同的类中不会有命名相同的两个方法)。

    • 对应的数据结构:
      typedef struct objc_selector *SEL;
      我们可以看出它是个映射到方法的C字符串,你可以通过Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。

    • 注意:不同类中相同名字的方法所对应的selector是相同的,由于变量的类型不同(即实例变量所对应的类的类型不同如NSString、NSMutableString等类型不同),所以不会导致它们调用方法实现的混乱。

    2.id
    id 是一个参数类型,它是指向某个类的实例的指针。定义如下:

    typedef struct objc_object *id;
    struct objc_object { Class isa; };
    

    通过以上定义,可以看到:objc_object结构体包含一个isa指针,根据isa指针就可以找到对象所属的类。

    注意:isa指针在代码运行时并不总是指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型需要用对象的 - class方法。

    3.Class

    typedef struct objc_class *Class;
    

    Class 其实是指向 objc_class 结构体的指针。objc_class的数据结构如下:

     struct objc_class {
         Class isa;//指针,顾名思义,表示是一个什么,
          //实例的isa指向类对象,类对象的isa指向元类
    
       #if !__OBJC2__
          Class super_class;  //指向父类
          const char *name;  //类名
          long version; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
          long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
          long instance_size;  // 该类的实例变量大小(包括从父类继承下来的实例变量);
          struct objc_ivar_list *ivars // 成员变量列表
          struct objc_method_list **methodLists; // 方法列表
          struct objc_cache *cache;// 缓存,存储最近使用的方法指针,用于提升效率
          struct objc_protocol_list *protocols // 协议列表
          #endif
      } OBJC2_UNAVAILABLE;
      /* Use `Class` instead of `struct objc_class *` */
    

    从结构体可以看出,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。
    其中objc_ivar_list和objc_method_list 分别是成员变量列表和方法列表:

    // 成员变量列表
    struct objc_ivar_list {
        int ivar_count                                           OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    
    // 方法列表
    struct objc_method_list {
        struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    
        int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }
    



    我们都知道,OC中一切都被设计成对象,一个类被初始化成一个实例,这个实例是一个对象。实际上一个类的本质也是一个对象,在runtime中用如上结构体表示。
    关于isa指针:

     比如 : NSString *tmpStr = [NSString string];
    

    这里的tmpStr的isa指针指向类对象NSString,而NSString的isa指针指向元类NSObject.

    4. Method
    Method 代表类中某个方法的类型

    typedef struct objc_method *Method;
    
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }
    

    objc_method存储了方法名、方法类型和方法实现:

    • 方法名类型为SEL

    • 方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型

    • method_imp 指向了方法的实现,本质是一个函数指针

    5.Ivar
    Ivar 是成员变量的类型。

    typedef struct objc_ivar *Ivar;
    
    struct objc_ivar {
        char *ivar_name                                     // 变量名称;
        char *ivar_type                                       // 变量类型;
        int ivar_offset                                         // 基地址偏移字节;
    #ifdef __LP64__
        int space                                               // 大小;
    #endif
    }
    

    其中 ivar_offset 是基地址偏移字节

    6.IMP
    IMP在objc.h中的定义是:
    typedef id (*IMP)(id, SEL, ...);
    它是一个函数指针,这是由编译器生成的。当你发送一个objc消息之后,最终它会执行那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。

    我们发现IMP指向的方法和objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的实现方法地址。所以一个确定的方法也只有一组id和SEL参数:
    比如:

    NSString *tmpStr = [NSString string];
    BOOL isContain =  [tmpStr containsString:@"1"];
    

    这里的containsString的实现方法就是由id(NSString)和SEL参数(containsString)确定的。

    7.Cache

    typedef struct objc_cache *Cache
    
    struct objc_cache {
        unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        unsigned int occupied                                    OBJC2_UNAVAILABLE;
        Method buckets[1]                                        OBJC2_UNAVAILABLE;
    };
    

    Cache主要用来提高查找效率,当一个方法被调用,首先在Cache列表中查找,如果找到直接返回,如果没有找到,再到类的方法列表去查找,找到了将该方法返回同时存入缓存列表。

    8.Property

    typedef struct objc_property *Property;
    typedef struct objc_property *objc_property_t;//这个更常用
    
    可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:
    
    objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
    

    注意返回的是属性列表,列表中的每个元素都是一个objc_property_t指针

    • 总结:

      // 描述类中的一个方法
      typedef struct objc_method *Method;
      
      // 实例变量
      typedef struct objc_ivar *Ivar;
      
      // 缓存(类方法)
      typedef struct objc_cache *Cache;
      
      // 实现 方法
      typedef id (*IMP)(id, SEL, ...);
      
      // 类别Category
      typedef struct objc_category *Category;
      
      // 类中声明的属性
      typedef struct objc_property *objc_property_t;
      
    树2.jpg

    四.获取列表

    有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的key和模型对象的属性名字不匹配)。
    我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表、方法列表、成员变量列表和遵循的协议列表)

    unsigned int count;
    //获取属性列表
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
    }
    
    //获取方法列表
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Method method = methodList[i];
        NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
    }
    
    //获取成员变量列表
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
    }
    
    //获取协议列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
    }
    

    五.方法调用

    方法调用在运行时的过程:

    • 如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
      比如:NSString *tmpStr = [NSString string];这里的 [NSString string]调用的是NSString的类方法,就会到NSString类对象的isa指针指向的对象NSObject元类中操作。

    • 如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
      比如: BOOL isContain = [tmpStr containsString:@"1"];这里的 [tmpStr containsString:@"1"],就是实例对象tmpStr调用containsString这个实例方法,得到实例tmpStr的isa指针指向的对象也就是类对象NSString操作。

      NSString *tmpStr = [NSString string];
      BOOL isContain =  [tmpStr containsString:@"1"];
      


    1. 首先,在相应操作对象中的缓存列表中查找调用的方法,如果找到,转向相应的实现并执行.(即先在tmpStr这个对象的缓存列表中查找是否有containsString这个方法,如果有,则转向相应的实现函数,并执行);

    2.如果没有找到,在相应操作对象的方法中找调用的方法,如果找到,转向相应的实现并执行。(即tmpStr的缓存列表找没有找到containsString这个方法,就到tmpStr的方法列表里面查找,如果找到,则转向相应的实现函数,并执行);

    3.如果没找到,去父类指针所指向的对象中执行1,2(即如果在tmpStr的方法列表里面没有找到containsString这个方法,则转向tmpStr的父类,也就是NSObject类去查找该方法)

    4.以此类推,如果一直到根类都还没找到,转向拦截调用(即如果在父类NSObject里面也没有找到containsString这个方法,就往上一层父类再去查找,知道最顶层(根层)父类,因为NSObject在OC中是根层父类,所以如果在NSObjec的方法列表找没找到containsString,就转向拦截调用)

    5.如果没有重写拦截调用的方法,程序报错。(即在tmpStr及其父类的方法列表中都没有containsString这个方法,就转向拦截调用,但是却没有实现拦截调用的方法,系统就报错)

    所以:

    • 重写父类的方法,并没有覆盖父类的方法,只是在当前类对象中找到了这个方法后,就不会再去父类中寻找了。

    • 如果子类重写父类的方法,但也想调用父类方法的实现,只需使用super这个编译器标识,它会在运行时先去调用父类的方法,在执行之类的方法。
      比如我们很常见的viewWillAppear函数:

      - (void)viewWillAppear:(BOOL)animated {
          [super viewWillAppear:animated];
          [self.navigationController setNavigationBarHidden:YES animated:animated];
      }
      

    这里的super标识,在运行时会先去执行父类的viewWillAppear方法,执行完毕之后,在回来接着执行原函数里面的隐藏导航栏的操作,如果没有 [super viewWillAppear:animated];就不会去调用父类的该方法,直接执行隐藏导航栏的操作。

    六.拦截调用

    拦截调用就是在找不到调用方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理,防止崩溃发生。

    消息转发顺序图.png
      + (BOOL)resolveClassMethod:(SEL)sel;
    

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

      + (BOOL)resolveInstanceMethod:(SEL)sel;
    

    这个方法和上一个方法相似,处理的是实例方法。(备注: NSString *tmpStr = [NSString string];像 [NSString string]这里的string就是类方法, [tmpStr containsString:@"1"]这里的containsString就是实例方法。)

      - (id)forwardingTargetForSelector:(SEL)aSelector;
    

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

      - (void)forwardInvocation:(NSInvocation *)anInvocation;
    

    该方法将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用inovkeWithTarget:方法让某个target触发这个方法。

    树3.jpg

    七.动态添加方法

    重写了拦截调用的方法并且返回YES,接下来可以根据传入的SEL类型的selector,动态添加一个方法。

    首先从外部隐式调用一个不存在的方法:

    // 隐式调用方法
    [target performSelector:@selector(resolveAdd:) withObject:@"test"];
    

    然后,在target对象内部重写拦截调用的方法,动态添加方法

    void runAddMethod(id self, SEL _cmd, NSString *string){
        NSLog(@"add C IMP ", string);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        //给本类动态添加一个方法
        if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
            class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
        }
        return YES;
    }
    

    其中class_addMethod的四个参数分别是:

    • Class cls 给哪个类添加方法,本例中是self
    • SEL name 添加的方法,本例中是重写拦截调用传进来的selector
    • IMP imp 方法的实现,C方法的实现可以直接获得。如果是OC方法,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
    • "v@:*"方法的签名,代表有一个参数的方法

    八.关联对象

    比如现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
    这种情况的一般解决办法就是继承。
    但是,只增加一个属性,就去继承一个类,总结太麻烦,这时候runtime的关联属性就发挥它的作用了。

    1.首先定义一个全局变量,用它的地址作为关联对象的key

    static char kAssociatedObjectKey; 
    

    2.在NSObject+AssociatedObject.h里面添加新的属性

     NSObject+AssociatedObject.h
    
    @interface NSObject (AssociatedObject) 
    @property (nonatomic, strong) id associatedObject; 
    @end 
    

    3.在NSObject+AssociatedObject.m里面添加设置和获取方法

     //设置关联对象
    @implementation NSObject (AssociatedObject) 
    @dynamic associatedObject; 
    
    - (void)setAssociatedObject:(id)object { 
         objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
    } 
    
    //获取关联对象
    - (id)associatedObject { 
        return objc_getAssociatedObject(self, @selector(associatedObject)); 
    } 
    

    objc_setAssociatedObject的四个参数:

    • id object 给谁设置关联对象

    • const void *key 关联对象唯一的key,获取时会用到

    • id value 关联的对象

    • objc_AssociationPolicy 关联策略,有以下几种策略:

      enum {
          OBJC_ASSOCIATION_ASSIGN = 0, // 给关联对象指定弱引用
          OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,  // 给关联的对象指定非原子操作哦
          OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 给关联对象指定费原子的copy特性
          OBJC_ASSOCIATION_RETAIN = 01401, // 给关联的对象指定原子的强引用
          OBJC_ASSOCIATION_COPY = 01403  // 给关联的对象指定原子的copy 特性
      };
      

    objc_getAssociatedObject 的两个参数:

    • id object 获取谁的关联对象
    • const void *key 根据这个唯一的key获取关联对象。

    其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能类的类别里面,方便调用。

    //添加关联对象
    - (void)addAssociatedObject:(id)object{
        objc_setAssociatedObject(self, @selector(getAssociatedObject), object,   OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    //获取关联对象
    - (id)getAssociatedObject{
        return objc_getAssociatedObject(self, _cmd);
    }
    
    • getAssociatedObject 方法的地址作为唯一的key,
    • _cmd代表当前调用方法的地址,也就是getAssociatedObject方法的地址

    ** 注意 :**
    4.移除关联对象:
    objc_removeAssociatedObjects()这个函数很容易让对象恢复成它"原始状态",你不应该使用它来移除关联的对象,因为它也会移除包括其他地方加入的全部关联对象。所以你一般只需要通过调用objc_setAssociatedObject并传入nil值类清除关联值。

    优秀样例

    • 添加私有属性用于更好地去实现细节。当扩展一个内建类的行为时,保持附加属性的状态可能非常必要。注意以下说的是一种非常教科书式的关联对象的用例:AFNetworking在 UIImageView
      的category上用了关联对象来保持一个operation对象,用于从网络上某URL异步地获取一张图片。

    • 添加public属性来增强category的功能。有些情况下这种(通过关联对象)让category行为更灵活的做法比在用一个带变量的方法来实现更有意义。在这些情况下,可以用关联对象实现一个一个对外开放的属性。回到上个AFNetworking的例子中的 UIImageView
      category,它的 imageResponseSerializer
      方法允许图片通过一个滤镜来显示、或在缓存到硬盘之前改变图片的内容。

    • 创建一个用于KVO的关联观察者。当在一个category的实现中使用KVO时,建议用一个自定义的关联对象而不是该对象本身作观察者。

    错误模式

    在不必要的时候使用关联对象。使用视图时一个常见的情况是通过数据模型或一些复合的值来创建一个便利的方法设置填充字段或属性。如果这些值在后面不会再被使用到,最好就不要使用关联对象了。(比如你将自定义的UITableViewCell跟模型关联起来,但这个cell值用在一个ViewController里面,也就是说这个关联对象只用到一处,之后就不再使用,这种情况下就没必要使用关联对象)。

    使用关联对象来保存一个可以被推算出来的值。例如,有人可能想通过关联对象存储UITableViewCell上一个自定义accessoryView的引用,使用tableView:accessoryButtonTappedForRowWithIndexPath: 和 cellForRowAtIndexPath:即可以达到要求。

    使用关联对象来代替X。其中X代表下面的一些项:
    子类化,当使用继承比使用组合更合适的时候。

    Target-Action给响应者添加交互事件。

    手势识别,当target-action模式不够用的时候。

    代理,当事件可以委托给其他对象。

    消息 & 消息中心使用低耦合的方式来广播消息。

    树4.jpg

    九.方法交换

    顾名思义:就是将两个方法的实现交换,比如,将A方法和B方法交换,调用A方法的时候,就回去执行B方法中的代码,反之亦然。
    参考Mattt Thompson的[Method Swizzling]文章:

    #import <objc/runtime.h>
    
    @implementation UIViewController (Tracking)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
    
            SEL originalSelector = @selector(viewWillAppear:);
            SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
            // When swizzling a class method, use the following:
            // Class class = object_getClass((id)self);
            // ...
            // Method originalMethod = class_getClassMethod(class, originalSelector);
            // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
    
            BOOL didAddMethod =
                class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
            if (didAddMethod) {
                class_replaceMethod(class,
                    swizzledSelector,
                    method_getImplementation(originalMethod),
                    method_getTypeEncoding(originalMethod));
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    #pragma mark - Method Swizzling
    
    - (void)xxx_viewWillAppear:(BOOL)animated {
        [self xxx_viewWillAppear:animated];
        NSLog(@"viewWillAppear: %@", self);
    }
    
    @end
    

    在自己定义的viewController中重写viewWillAppear

    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        NSLog(@"viewWillAppear");
    }
    

    就会调用xxx_viewWillAppear,输出log, method swizzling在视图控制器的生命周期、响应事件、绘制视图或者Foundation框架的网络栈等方法中需要插入代码的时候,都是很好的解决方法。

    +load vs +initialize:

    swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。

    • +load 是在一个类被初始装载时调用(iOS应用启动的时候,就会加载所有的类,就会调用这个方法);并且因为加载进内存只会加载一次,所以也一般也只会调用一次

    • +initialize 是在应用第一次调用该类的类方法或实例方法前调用的,调用次数根据子类和具体调用情况确定。

    dispatch_once:

    swizzling 应该只在 dispatch_once 中完成。

    由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 满足了所需要的需求,并且应该被当做使用 swizzling 的初始化单例方法的标准。

    Selectors, Methods, & Implementations
    苹果定义:

    Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。

    Method(typedef struct objc_method *Method):方法是一个不透明的用来代表一个方法的定义的类型。

    Implementation(typedef id (*IMP)(id, SEL,...)):这个数据类型指向一个方法的实现的最开始的地方。该方法为当前CPU架构使用标准的C方法调用来实现。该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象metaclass)。第二个参数是这个方法的名字selector,该方法的真正参数紧随其后。

    三者之间的关系:

    在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键值是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。 Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射了另一个实现 implementation,同时重命名了原生方法的实现为一个新的 selector。

    也就是说swizzling只是交换两个方法在函数表中的指向地址而已。

    调用 _cmd

    - (void)xxx_viewWillAppear:(BOOL)animated {
        [self xxx_viewWillAppear:animated];
        NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
    }
    

    初看这段代码,我们都会觉得会出现递归死循环。但事实不是这样的:

    method swizzling 在交换方法的实现后,xxx_viewWillAppear:方法的实现已经被替换为UIViewController 的-viewWillAppear:这个原生方法。

    所以当我们在UIViewController调用这个- (void)viewWillAppear:(BOOL)animated 方法的时候,实际上调用的是xxx_viewWillAppear这个方法,而 [self xxx_viewWillAppear:animated];这个方法实际上调用的是系统的viewWillAppear。

    这就证实了swizzling只是交换两个方法在函数表中的指向地址而已。

    常见坑

    1. Method swizzling 是非原子性的,在多线程环境下可能被多次修改,但同样 Method swizzling 又是全局性的,就会造成不可预知的错误。

    2. 可能出现命名冲突的问题,这样就不会调用到系统原方法,可能导致未知问题。

    3. Method swizzling 看起来像递归,对新人来说不容易理解。

    4. 出现问题 Method swizzling 不容易进行debug,来发现问题

    5. 随着项目迭代和人员更换,使用Method swizzling 的项目不容易维护,因为开发人员有时根本不知道在Method swizzling 里面修改了东西。

    预防措施

    • 在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
    • 避免冲突:为分类的方法加前缀,一定要确保调用了原生方法的所有地方不会因为你交换了方法的实现而出现意想不到的结果。
    • 理解实现原理: 只是简单的拷贝粘贴交换方法实现的代码而不去理解实现原理不仅会让 App 很脆弱,并且浪费了学习 Objective-C 运行时的机会。阅读 Objective-C Runtime Reference 并且浏览 能够让你更好理解实现原理。
    • 持续的预防: 不管你对你理解 swlzzling 框架,UIKit 或者其他内嵌框架有多自信,一定要记住所有东西在下一个发行版本都可能变得不再好使。做好准备,在使用这个黑魔法中走得更远,不要让程序反而出现不可思议的行为。

    十.感想

    runtime是把双刃剑,因为所有的代码都运行在它之上,改变它,可能会改变代码的正常运行逻辑和所有与之交互的东西,因此会产生可怕的副作用。但同时它强大的功能也可以给应用的框架或者代码的编写带来非常大的便利。

    因此,对于runtime唯一的建议就是,需谨慎使用,一旦使用,必须先了解runtime的相关原理,做好预防措施,在添加完自己的代码之后,一定要调用系统原来的方法。

    十一.最后:

    送上一张喜欢的图片:

    树5.jpg

    提醒:不应该把runtime的使用看成是高大上的东西,并以使用这个为荣,实际开发中runtime能少用应该少用,正常的系统方法才是正道!

    这是一篇总结笔记,大家有兴趣可以蛮看一下,如果觉得不错,麻烦给个喜欢或star,若发现有错误的地方请及时反馈,谢谢!

    相关文章

      网友评论

        本文标题:Runtime (整理笔记)

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