iOS底层原理19:KVC分析

500 阅读11分钟

这是我参与8月更文挑战的第8天,活动详情查看: 8月更文挑战

Method Swizzling 应用

Method Swizzling 简介

Method Swizzling被称为苹果的黑魔法,其本质上是对SELIMP进行交换;Method Swizzling发生在运行时,主要用于在运行时将两个Method进行交换;而且它也是iOSAOP(面向切面编程)的一种实现方式,我们可以利用这一特性来实现AOP编程;

Method Swizzling 原理图解

图一中,oriSEL原本对应着oriIMPswiSEL对应着swiIMP;但是由于某些业务需求,我们需要将两个方法的实现进行交换,于是就有了图二,将oriSEL指向swiIMP,而swiSEL指向了oriIMP的情况,这就是方法交换

Objective-C语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息,通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,而这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用;

在每个中都有一个Dispatch Table,其本质是将中的SELIMP(可以理解为函数指针)进行对应,而Method Swizzling就是对这个table进行了操作,让SEL对应另外的IMP

其核心方法为:

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

Method Swizzling 应用

我们先封装一个Method Swizzling的交换方法:

+ (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);
}

接下来,我们运用这个方法来进行方法交换:

Person

@interface Person : NSObject
- (void)personInstanceMethod;
@end

@implementation Person
- (void)personInstanceMethod {
    NSLog(@"person对象方法:%s",__func__);
}
@end

Student

@interface Student : Person

@end

@implementation Student

@end

Student的分类

@interface Student (MS)

@end

@implementation Student (MS)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [RuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(studentInstanceMethod)];
    });
}

// 是否会递归?
- (void)studentInstanceMethod{
    [self studentInstanceMethod];
    NSLog(@"Student分类添加的对象方法:%s",__func__);
}

@end

注意此处不会递归,因为我们在load方法中已经将personInstanceMethodstudentInstanceMethod进行了交换,所以这里的[self studentInstanceMethod]其实是调用了personInstanceMethod方法,因此不会造成递归调用;

ViewController.m中实现如下方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    Student *s = [[Student alloc] init];
    [s personInstanceMethod];
    
}

看一下打印结果:

这就是一个简单的方法交换的实现;

Method Swizzling 注意事项

load 和 initialize

Objective-C运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。因此Method Swizzling可以写在这两个方法中;(推荐写在load方法中)

dispatch_once

Method Swizzling应该在dispatch_once中完成。

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

Method Swizzling 坑点

坑点1 父类方法被交换,父类调用自身方法崩溃

但是,仅仅做到这些还是不够的,我们来看下边代码的运行结果:

原因:由于方法personInstanceMethod已经被交换成studentInstanceMethod方法,所以在Person的对象调用personInstanceMethod时,实质上是在调用studentInstanceMethod,而Person并无此方法的实现,因此必然崩溃;

我们对lg_methodSwizzlingWithClass进行改进:

+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    // oriSEL       personInstanceMethod
    // swizzledSEL  studentInstanceMethod
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    /**
     * 尝试添加要交换的方法 studentInstanceMethod
     * 如果添加成功,则说明类中没有此方法的实现,因此需要给类重新写一个方法实现,来替换原来的 personInstanceMethod(sel) 对应 tudentInstanceMethod(imp)
     * 如果没有添加成功,则说明类中有此方法的实现,那么交换方法的实现 studentInstanceMethod (swizzledSEL) 对应 personInstanceMethod(imp)
     */
    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);
    }
}

运行结果:

坑点2 父类方法没有实现,子类交换崩溃

我们将Person的方法personInstanceMethod注释之后,运行结果:

产生了递归,崩溃;

因为Person类并没有实现personInstanceMethod,所以在进行操作时oriMethod不存在,那么方法交换将会失败,最终[self studentInstanceMethod]产生了递归调用;

我们继续对lg_betterMethodSwizzlingWithClass方法进行优化:

+ (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){
            NSLog(@"来了一个空的 imp");
        }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    // 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    // 然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    // oriSEL:personInstanceMethod

    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);
    }
    
}

然后看执行结果:

KVC简介

KVC的全称是Key-Value Coding,意思就是键值编码键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。既可以通过一个字符串key来访问某个性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。

KVC相关Api

  • 通过key操作
// 通过key来取值
- (nullable id)valueForKey:(NSString *)key;
// 通过key来设置value
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过keyPath操作
// 通过keyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 通过keyPath来设置value
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
  • 其他相关方法
// //默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 如果需要设值的key都不存在,最终会调用此方法
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC底层原理

设值的底层原理

我们经常给一个对象的属性赋值是通过属性setter方法赋值,其实还可以通过KVC键值编码进行赋值:

Person *p = [[Person alloc] init];
// 属性的setter方法赋值
p.name = @"张三";
// KVC赋值
[p setValue:@"张三" forKey:@"name"];

那么,KVC赋值的底层逻辑是什么呢?我们以- (void)setValue:(nullable id)value forKey:(NSString *)key方法为例,来进行底层原理的探索;

KVC的定义是通过NSObject的分类NSKeyValueCoding来实现的;由于setValue:forKey:方法在Foundation框架中,而Foundation框架是不开源的,所以我们这里结合 Key-Value Coding Programming Guide的官方文档进行探索研究;

当调用setValue:forKey:时,根据官方文档说明

可以分析出其执行流程为:

  • 1、首先按照顺序查找是否有set<key>_set<key>setIsName三个setter方法;

    • 如果含有其中任何一个setter方法,则直接调用setter方法赋值;
    • 如果三个setter方法都没有,则执行第2步;

    代码验证如下:

  • 2、查找accessInstanceVariablesDirectly方法是否返回YES(默认为YES);

    • 如果返回YES,则查找相应的实例变量进行赋值,查找顺序为_<key>_is<key><key>is<key>;如果实例变量都没有找到,那么执行第3步;
    • 如果返回NO,则执行第3步;

    代码验证如下:

  • 3、如果上述两步setter方法和实例变量都没有找到,那么系统将执行当前对象的setValue:forUndefinedKey:方法,默认抛出一个异常:NSUndefinedKeyException

    代码验证如下:

KVC设值的流程为:

取值的底层原理

当调用valueForKey:时,根据官方文档:

可以分析出其执行流程为:

  • 1、按照顺序查找get<Key><key>is<Key>_<key>四个方法
    • 如果找到,则执行第5步;
    • 如果没有找到,则执行第2步;
  • 2、如果第1步中的方法都没有找到,KVC会查找countOf<Key>objectIn<Key>AtIndex:以及<key>AtIndexes
    • 如果找到countOf<Key>和其他两个中的任一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray,它是NSArray的一个子类。代理对象随后将接收到的所有NSArray的消息转化为countOf<Key>objectIn<Key><key>AtIndexes消息的组合,用来创建键值编码对象。如果原始对象还实现了名为get<Key>:range:之类的可选方法,则代理对象也将在适当的时机使用该方法;
    • 如果没有找到这三个方法,则执行第3步;
  • 3、如果没有找到上述方法,则查找countOf<Key>enumeratorOf<Key>memberOf<Key>:方法:
    • 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将收到的所有的NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的组合,用于创建它的对象;
    • 如果没有找到,则执行第4步;
  • 4、如果还没有找到,则检查类方法accessInstanceVariablesDirectly是否返回YES
    • 如果返回YES,则依次查找名为_<key>_is<Key><key>is<Key>的实例变量,如果找到,则执行第5步,如果未找到,则执行第6步;
    • 如果返回NO,则执行第6步;
  • 5、根据找到的属性值的类型,返回相应的结果:
    • 如果是对象指针,则直接返回结果;
    • 如果是NSNumber支持的标量类型,则将其存储在NSNumber的实例并返回;
    • 如果是NSNumber不支持的标量类型,则转换为NSValue对象并返回;
  • 6、如果前边都没有找到,系统会执行该对象的valueForUndefinedKey:方法,默认抛出一个异常:NSUndefinedKeyException

KVC取值的流程为:

自定义KVC实现

根据上述规则,我们可以自定义一个KVC的实现流程,代码如下:

@implementation NSObject (LGKVC)

- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
   
    // KVC 自定义
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set<Key>: or _set<Key>,
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    
    // 5:如果找不到相关实例
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}


- (nullable id)lg_valueForKey:(NSString *)key{
    
    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}


#pragma mark - 相关方法
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

@end