美文网首页
Method Swizzling & KVC

Method Swizzling & KVC

作者: 深圳_你要的昵称 | 来源:发表于2020-10-28 17:29 被阅读0次

    一、Method Swizzling(方法交换)

    Method Swizzling本质上就是对方法的IMPSEL进行交换,也是我们常说的黑魔法之一。

    1.1 方法交换的原理

    • Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换。
    • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法即Method,每个方法中包含了方法的SELIMP,方法交换就是将原本的SELIMP对应断开,并将SEL新的IMP生成对应关系。
    • 而且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

    方法的SELIMP的交换如下图👇

    1.2使用时需注意的坑点

    1.2.1method-swizzling 多次调用的混乱问题

    我们都知道,mehod-swizzling写在load方法中,而load方法会被系统主动调用多次,这样会导致方法的重复交换,第1次交换,第2次又还原,第3次又交换,这不就乱套了吗?所以,我们得保证方法交换的触发有且仅有1次。

    解决方案
    当然是单例模式。👇

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
           //TODO:这里进行你的方法交换
        });
    }
    
    1.2.2 子类没有实现,父类实现了

    父类实现了方法交换,但是其子类却没有,例如👇

    //*********LGPerson类*********
    @interface LGPerson : NSObject
    - (void)personInstanceMethod;
    @end
    
    @implementation LGPerson
    - (void)personInstanceMethod{
        NSLog(@"person对象方法:%s",__func__);  
    }
    @end
    
    //*********LGStudent类*********
    @interface LGStudent : LGPerson
    
    @end
    
    @implementation LGStudent
    @end
    
    //*********调用*********
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 黑魔法坑点二: 子类没有实现 - 父类实现
        LGStudent *s = [[LGStudent alloc] init];
        [s personInstanceMethod];
        
        LGPerson *p = [[LGPerson alloc] init];
        [p personInstanceMethod];
    }
    

    然后,方法交换的代码👇

    @implementation LGStudent (LG)
    
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
        });
    }
    
    // personInstanceMethod 我需要父类的这个方法的一些东西
    // 给你加一个personInstanceMethod 方法
    // imp
    
    - (void)lg_studentInstanceMethod{
        ////是否会产生递归?--不会产生递归,原因是lg_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
        [self lg_studentInstanceMethod];
        NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
    }
    
    @end
    

    其中,封装的方法交换代码👇

    @implementation LGRuntimeTool
    + (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    

    run运行项目


    红框处刻制,在类LGPerson找不到方法lg_studentInstanceMethod的实现。
    原因在于,LGStudent的分类中进行了方法交换,将LGPerson类中的方法personInstanceMethod的实现imp交换成了LGStudent类中的lg_studentIndtanceMethod,然后[p personInstanceMethod];实际是去LGPerson类中找lg_studentInstanceMethod的imp,但是没有,就引起崩溃。

    解决方案
    上述崩溃的根本原因是找不到方法的实现imp,假如我们在方法交换时,先判断交换的方法是否有实现imp,未实现则先实现,这样不就解决问题了。那么,优化交换方法,改为替换方法👇

    + (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) NSLog(@"传入的交换类不能为空");
        // oriSEL       personInstanceMethod
        // swizzledSEL  lg_studentInstanceMethod
        
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
       
        // 尝试添加你要交换的方法 - lg_studentInstanceMethod
        BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
        
        if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{ // 自己有
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
    

    在交换的load方法处调用👇

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
        });
    }
    

    这样就完美解决了!


    1.2.3 子类没有实现,父类也没有实现

    将父类的personInstanceMethod方法注释

    //*********LGPerson类*********
    @interface LGPerson : NSObject
    - (void)personInstanceMethod;
    @end
    
    @implementation LGPerson
    //- (void)personInstanceMethod{
    //    NSLog(@"person对象方法:%s",__func__);
    //}
    @end
    

    run运行项目


    很明显递归死循环导致栈溢出,那么为什么会发生递归呢?
    主要是因为 personInstanceMethod没有实现,然后在方法交换时,始终都找不到oriMethod,然后交换了寂寞,即交换失败,当我们调用personInstanceMethod(oriMethod)时,也就是oriMethod会进入LG中lg_studentInstanceMethod方法,然后这个方法中又调用了lg_studentInstanceMethod,此时的lg_studentInstanceMethod并没有指向personInstanceMethod(oriMethod) ,然后导致了自己调自己,即递归死循环。

    解决方案
    避免递归死循环 -->增加判断:如果oriMethod为空,可通过class_addMethodoriSEL添加swiMethod方法,然后通过method_setImplementationswiMethod实现IMP指向不做任何事的空实现。
    例如:将上述方法lg_bestMethodSwizzlingWithClass改成👇

    + (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) NSLog(@"传入的交换类不能为空");
        
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
        
        if (!oriMethod) {
            // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
            class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
        }
    
        BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    }
    
    1.2.4 Method Swizzling类簇

    在我们平时项目开发过程中,经常碰到NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的crash,对于这些问题苹果爸爸并不会报一个警告,而是直接崩溃,不给你任何机会补救。

    因此,我们可以根据这个方法交换,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,发现越界或值为nil时,做一些补救措施,实现方式还是按照上面的例子来做。但是,你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?

    因为它们是类簇Method Swizzling类簇不起作用!

    那什么是类簇呢?-->是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。

    类名 真身
    NSArray __NSArrayI
    NSMutableArray __NSArrayM
    NSDictionary __NSDictionaryI
    NSMutableDictionary __NSDictionaryM

    示例代码

    @implementation NSArray (CJLArray)
    //如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
    + (void)load{
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:));
        
        method_exchangeImplementations(fromMethod, toMethod);
    }
    
    //如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
    - (id)cjl_objectAtIndex:(NSUInteger)index{
        //判断下标是否越界,如果越界就进入异常拦截
        if (self.count-1 < index) {
            // 这里做一下异常处理,不然都不知道出错了。
    #ifdef DEBUG  // 调试阶段
            return [self cjl_objectAtIndex:index];
    #else // 发布阶段
            @try {
                return [self cjl_objectAtIndex:index];
            } @catch (NSException *exception) {
                // 在崩溃后会打印崩溃信息,方便我们调试。
                NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
                NSLog(@"%@", [exception callStackSymbols]);
                return nil;
            } @finally {
                
            }
    #endif
        }else{ // 如果没有问题,则正常进行方法调用
            return [self cjl_objectAtIndex:index];
        }
    }
    
    @end
    

    二、KVC

    2.1KVC概念

    Key Value Coding 也即 KVC,是 iOS 开发中一个很重要的知识点,中文翻译过来是 键值对编码 ,关于这个概念的具体定义可以在苹果的官方文档处找到。

    2.2KVC基本用法

    2.2.1 直接对属性或者成员变量进行取值和赋值
    @interface LGPerson : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    // 调用
    LGPerson *person = [[LGPerson alloc] init];
    // 一般setter情况
    person.name = @"LG_Cooci";
    // 非正式协议 - 间接访问
    [person setValue:@"KC" forKey:@"name"];
    
    2.2.2 针对集合属性,可以通过mutableArrayValueForKey对其进行操作更改
    @interface LGPerson : NSObject
    @property (nonatomic, strong) NSArray           *array;
    @end
    
    // 调用
    person.array = @[@"1",@"2",@"3"];
    // 第一种:搞一个新的数组 - KVC 赋值就OK
    NSArray *array = [person valueForKey:@"array"];
    array = @[@"100",@"2",@"3"];
    [person setValue:array forKey:@"array"];
    NSLog(@"%@",[person valueForKey:@"array"]); 
    // 第二种
    NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
    mArray[0] = @"200";
    NSLog(@"%@",[person valueForKey:@"array"]);
    
    2.2.3 针对结构体,此时需将结构体转成NSValue类型
    typedef struct {
        float x, y, z;
    } ThreeFloats;
    @interface LGPerson : NSObject
    @property (nonatomic) ThreeFloats threeFloats;
    @end
    
    // 调用
    ThreeFloats floats = {1.,2.,3.};
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *value1    = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",value1);
        
    ThreeFloats th;
    [value1 getValue:&th]; 
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);
    
    2.2.4 对象的属性也是对象,可以通过keyPath来操作

    新建一个类LGStudent,继承LGPerson

    @interface LGStudent : NSObject
    @property (nonatomic, copy)   NSString          *name;
    @property (nonatomic, copy)   NSString          *subject;
    @property (nonatomic, copy)   NSString          *nick;
    @property (nonatomic, assign) int               age;
    @property (nonatomic, assign) int               length;
    @property (nonatomic, strong) NSMutableArray    *penArr;
    @end
    
    @implementation LGStudent
    
    @end
    

    LGPerson中声明对象属性:

    @interface LGPerson : NSObject
    @property (nonatomic, strong) LGStudent         *student;
    @end
    
    // 调用
    LGStudent *student = [LGStudent alloc];
    student.subject    = @"大师班"; 
    person.student     = student;
        
    [person setValue:@"Swift" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
    
    2.2.5 setValuesForKeysWithDictionary & dictionaryWithValuesForKeys
        NSDictionary* dict = @{
                               @"name":@"Cooci",
                               @"nick":@"KC",
                               @"subject":@"iOS",
                               @"age":@18,
                               @"length":@180
                               };
        LGStudent *p = [[LGStudent alloc] init];
        // 字典转模型
        [p setValuesForKeysWithDictionary:dict];
        NSLog(@"%@",p);
        // 键数组转模型到字典
        NSArray *array = @[@"name",@"age"];
        NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
        NSLog(@"%@",dic);
    
    2.2.6 数组相关的特殊api
        NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
        NSArray *lenStr= [array valueForKeyPath:@"length"];
        NSLog(@"%@",lenStr);// 消息从array传递给了string
        NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
        NSLog(@"%@",lowStr);
    
    2.2.7 聚合操作符
    // @avg、@count、@max、@min、@sum
    - (void)aggregationOperator{
        NSMutableArray *personArray = [NSMutableArray array];
        for (int i = 0; i < 6; i++) {
            LGStudent *p = [LGStudent new];
            NSDictionary* dict = @{
                                   @"name":@"Tom",
                                   @"age":@(18+i),
                                   @"nick":@"Cat",
                                   @"length":@(175 + 2*arc4random_uniform(6)),
                                   };
            [p setValuesForKeysWithDictionary:dict];
            [personArray addObject:p];
        }
        NSLog(@"%@", [personArray valueForKey:@"length"]);
        
        /// 平均身高
        float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
        NSLog(@"%f", avg);
        
        int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
        NSLog(@"%d", count);
        
        int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
        NSLog(@"%d", sum);
        
        int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
        NSLog(@"%d", max);
        
        int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
        NSLog(@"%d", min);
    }
    
    // 数组操作符 @distinctUnionOfObjects @unionOfObjects
    - (void)arrayOperator{
        NSMutableArray *personArray = [NSMutableArray array];
        for (int i = 0; i < 6; i++) {
            LGStudent *p = [LGStudent new];
            NSDictionary* dict = @{
                                   @"name":@"Tom",
                                   @"age":@(18+i),
                                   @"nick":@"Cat",
                                   @"length":@(175 + 2*arc4random_uniform(6)),
                                   };
            [p setValuesForKeysWithDictionary:dict];
            [personArray addObject:p];
        }
        NSLog(@"%@", [personArray valueForKey:@"length"]);
        // 返回操作对象指定属性的集合
        NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
        NSLog(@"arr1 = %@", arr1);
        // 返回操作对象指定属性的集合 -- 去重
        NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
        NSLog(@"arr2 = %@", arr2);
        
    }
    
    // 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
    - (void)arrayNesting{
        
        NSMutableArray *personArray1 = [NSMutableArray array];
        for (int i = 0; i < 6; i++) {
            LGStudent *student = [LGStudent new];
            NSDictionary* dict = @{
                                   @"name":@"Tom",
                                   @"age":@(18+i),
                                   @"nick":@"Cat",
                                   @"length":@(175 + 2*arc4random_uniform(6)),
                                   };
            [student setValuesForKeysWithDictionary:dict];
            [personArray1 addObject:student];
        }
        
        NSMutableArray *personArray2 = [NSMutableArray array];
        for (int i = 0; i < 6; i++) {
            LGPerson *person = [LGPerson new];
            NSDictionary* dict = @{
                                   @"name":@"Tom",
                                   @"age":@(18+i),
                                   @"nick":@"Cat",
                                   @"length":@(175 + 2*arc4random_uniform(6)),
                                   };
            [person setValuesForKeysWithDictionary:dict];
            [personArray2 addObject:person];
        }
        
        // 嵌套数组
        NSArray* nestArr = @[personArray1, personArray2];
        
        NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
        NSLog(@"arr = %@", arr);
        
        NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
        NSLog(@"arr1 = %@", arr1);
    }
    
    - (void)setNesting{
        
        NSMutableSet *personSet1 = [NSMutableSet set];
        for (int i = 0; i < 6; i++) {
            LGStudent *person = [LGStudent new];
            NSDictionary* dict = @{
                                   @"name":@"Tom",
                                   @"age":@(18+i),
                                   @"nick":@"Cat",
                                   @"length":@(175 + 2*arc4random_uniform(6)),
                                   };
            [person setValuesForKeysWithDictionary:dict];
            [personSet1 addObject:person];
        }
        NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"length"]);
        
        NSMutableSet *personSet2 = [NSMutableSet set];
        for (int i = 0; i < 6; i++) {
            LGPerson *person = [LGPerson new];
            NSDictionary* dict = @{
                                   @"name":@"Tom",
                                   @"age":@(18+i),
                                   @"nick":@"Cat",
                                   @"length":@(175 + 2*arc4random_uniform(6)),
                                   };
            [person setValuesForKeysWithDictionary:dict];
            [personSet2 addObject:person];
        }
        NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"length"]);
    
        // 嵌套set
        NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];
        // 交集
        NSArray* arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.length"];
        NSLog(@"arr1 = %@", arr1);
    }
    

    2.3 KVC设值 & 取值原理

    KVC在底层库Foundation中,由于Foundation库是闭源的,无法查看其源码,我们只有通过官方文档来看看KVC设值 & 取值的一个大致流程。

    2.3.1 KVC设值原理

    翻译出来,大致意思是:

    1. 先依次查询有没有相关的方法:set< Key>:、_set< Key>:、setIs< Key>: 找到直接进行调用赋值。
    2. 若没有相关方法时,会查看类方法accessInstanceVariablesDirectly是否为YES时进入下一步。否则进入步骤4
    3. 为YES时,可以直接访问成员变量的来进行赋值,依次寻找变量 _< key >、 _is< Key>、 < key>、 is< Key>。找到则直接赋值,否则进入下一步。
    4. 将会调用setValue:forUndefinedKey:方法进行抛出异常。可以自定义的实现为未找到的场景来避免抛出异常的行为。
    2.3.2 KVC取值原理

    上图是取值流程,大致分为6步:

    1. 以 get<Key>, <key>, is<Key> 以及 _<key> 的顺序查找对象中是否有对应的方法。
      • 如果找到了,将方法返回值带上跳转到第 5 步
      • 如果没有找到,跳转到第 2 步
    2. 查找是否有 countOf<Key> 和 objectIn<Key>AtIndex: 方法(对应于 NSArray 类定义的原始方法)以及 <key>AtIndexes: 方法(对应于 NSArray 方法 objectsAtIndexes:)
      • 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray 方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:,要么是 countOf<Key> + <key>AtIndexes:,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
      • 如果没有找到,跳转到第 3 步
    3. 查找名为 countOf<Key>,enumeratorOf<Key> 和 memberOf<Key> 这三个方法(对应于NSSet类定义的原始方法)
      • 如果找到这三个方法,则创建一个响应所有 NSSet 方法的代理集合对象,并返回该对象
      • 如果没有找到,跳转到第 4 步
    4. 判断类方法 accessInstanceVariablesDirectly 结果
      • 如果返回 YES,则以 _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量,如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步
      • 如果返回 NO,跳转到第 6 步
    5. 判断取出的属性值
      • 如果属性值是对象,直接返回
      • 如果属性值不是对象,但是可以转化为 NSNumber 类型,则将属性值转化为 NSNumber 类型返回
      • 如果属性值不是对象,也不能转化为 NSNumber 类型,则将属性值转化为 NSValue 类型返回
    6. 调用 valueForUndefinedKey:。 默认情况下,这会引发一个异常,但是 NSObject 的子类可以提供特定于 key 的行为。

    2.4自定义KVC

    根据上面官方文档的设值和取值流程,我们可以尝试实现一个自定义KVC,大致代码如下:
    XFCustomKVC

    总结

    KVC的功能点有很多,我们在实际的开发过程中用到的点很少。所有的功能点总结如下图👇


    相关文章

      网友评论

          本文标题:Method Swizzling & KVC

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