美文网首页
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