KVC

在咱们的往常开发中经常用到KVC赋值取值字典转模型,但KVC的底层原理又是怎样的呢?

由于apple原生的Foundation.framework是不开源的,所以咱们是无法经过源码学习流程的!可是有个安排GNUstep复原完结Foundation的功用,咱们能够经过这部分源码了解KVCKVO原理。

一、KVC的初探

KVC的界说及API

KVC(Key-Value Coding)是运用NSKeyValueCoding,目标采用这种机制来供给对其特点的直接访问

  • 测验代码:
@interface CJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
        [person setValue: @"CJ" forKey: @"name"];
    [person setValue: [NSNumber numberWithInt:12] forKey: @"age"];
    }
    return 0;
}
  • 定论:

    依据测验代码的并点击跟进setValue会发现NSKeyValueCoding是在Foundation结构下,检查gnustep源码也可知。

    • ①. KVC经过对NSObject扩展来完结的 —— 一切集成了NSObject能够运用KVC

    • ②. NSArrayNSDictionaryNSMutableDictionaryNSOrderedSetNSSet等也遵守KVC协议

    • ③. 除少数类型(结构体)以外都能够运用KVC

  1. 在gnustep源码中能够看到NSObject(KeyValueCoding)中的KVC常用办法,这些也是咱们经常用到的:
    OC底层原理(十五)KVC与KVO
  • 常用办法:
// 经过 key 设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 经过 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 经过 keyPath 设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 经过 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
  • NSKeyValueCoding类别的其它办法:
// 默以为YES。 假如回来为YES,假如没有找到 set<Key> 办法的话, 会依照_key, _isKey, key, isKey的次序搜索成员变量, 回来NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 键值验证, 能够经过该办法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 假如key不存在, 并且没有搜索到和key有关的字段, 会调用此办法, 默许抛出反常。两个办法别离对应 get 和 set 的状况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue办法传 nil 时调用的办法
// 留意文档阐明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此办法 
- (void)setNilValueForKey:(NSString *)key;
// 一组 key对应的value, 将其转成字典回来, 可用于将 Model 转成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

主动生成的settergetter办法

试想一下编译器要为不计其数个特点别离生成settergetter办法那不得歇菜了嘛?

所以苹果开发者们就运用通用准则给一切特点都供给了同一个入口——objc-accessors.mm中setter办法依据修饰符不同调用不同办法,最终一致调用reallySetProperty办法。

OC底层原理(十五)KVC与KVO

  • 定论:

    来到reallySetProperty再依据内存偏移量取出特点,依据修饰符完结不同的操作。

    • ①. 在榜首个特点name赋值时,此时的内存偏移量为8,刚好偏移isa所占内存(8字节)来到name
      OC底层原理(十五)KVC与KVO
  1. 至所以哪里调用的objc_setProperty_nonatomic_copy

    并不是在objc源码中,而在llvm源码中发现了它,依据它一层层找上去就能找到源头。

    OC底层原理(十五)KVC与KVO

二、KVC的运用

1. 根本类型

留意一下NSInteger这类根本类型特点赋值时要转成NSNumberNSString

@interface CJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
    [person setValue: @"CJ" forKey: @"name"];
    [person setValue: [NSNumber numberWithInt:12] forKey: @"age"];
    NSLog(@"person's name is %@, the age is %@",[person valueForKey:@"name"], [person valueForKey:@"age"]);
    }
    return 0;
}
  • 打印成果:
2023-04-18 12:43:48.958195+0800 KCObjcBuild[45906:1531706] person's name is CJ, the age is 12

2. 调集类型

两种办法对数组进行赋值,更引荐运用第二种办法

@interface CJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSArray *family;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
    NSLog(@"简略的赋值: %@", person.family);
        // 直接用新的数组赋值
        NSArray *temp = @[@"CJSon", @"CJGrapa"];
        [person setValue:temp forKey:@"family"];
        NSLog(@"榜首次改动:%@", [person valueForKey:@"family"]);
        // 取出数组以可变数组办法保存,再修正
        NSMutableArray *mTemp = [person mutableArrayValueForKey:@"family"];
        [mTemp addObjectsFromArray:@[@"CJFather", @"CJMonther"]];
        NSLog(@"第2次改动:%@", [person valueForKey:@"family"]);
    }
    return 0;
}
  • 打印成果:
2023-04-18 13:09:35.190730+0800 KCObjcBuild[46567:1553828] 简略的赋值: (
    CJFather,
    CJMonther
)
2023-04-18 13:09:37.575757+0800 KCObjcBuild[46567:1553828] 榜首次改动:(
    CJSon,
    CJGrapa
)
2023-04-18 13:09:37.577047+0800 KCObjcBuild[46567:1553828] 第2次改动:(
    CJSon,
    CJGrapa,
    CJFather,
    CJMonther
)

3. 访问非目标类型(结构体)

关于非目标类型的赋值总是把它先转成NSValue类型再进行存储,取值时转成对应类型后再运用:

typedef struct {
    float x,y,z;
} TreeFloats;
@interface CJPerson : NSObject
@property (nonatomic, assign) TreeFloats threeFloats;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
    
        TreeFloats floats = {180.0, 140.0, 18.0};
        NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(TreeFloats)];
        [person setValue:value forKey:@"threeFloats"];
        NSLog(@"非目标类型:%@", [person valueForKey:@"threeFloats"]);
        TreeFloats ths;
        NSValue *currentValue = [person valueForKey:@"threeFloats"];
        [currentValue getValue:&ths];
        NSLog(@"非目标类型的值:%f-%f-%f", ths.x, ths.y, ths.z);
    }
    return 0;
}
  • 打印成果:
2023-04-18 13:33:22.869934+0800 KCObjcBuild[47095:1573683] 非目标类型:{length = 12, bytes = 0x0000344300000c4300009041}
2023-04-18 13:33:22.870186+0800 KCObjcBuild[47095:1573683] 非目标类型的值:180.000000-140.000000-18.000000

4. 调集操作符

  • 聚合操作符

    • @avg: 回来操作目标指定特点的平均值
    • @count: 回来操作目标指定特点的个数
    • @max: 回来操作目标指定特点的最大值
    • @min: 回来操作目标指定特点的最小值
    • @sum: 回来操作目标指定特点值之
  • 数组操作符

    • @distinctUnionOfObjects: 回来操作目标指定特点的调集–去重
    • @unionOfObjects: 回来操作目标指定特点的调集
  • 嵌套操作符

    • @distinctUnionOfArrays: 回来操作目标(嵌套调集)指定特点的调集–去重,回来的是NSArray
    • @unionOfArrays: 回来操作目标(调集)指定特点的调集
    • @distinctUnionOfSets: 回来操作目标(嵌套调集)指定特点的调集–去重,回来的是NSSet
  • 测验代码:

@interface CJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *persons = [NSMutableArray array];
        for (int i = 0; i < 10; i++) {
            CJPerson *p = [CJPerson new];
            NSDictionary *dict = @{
                @"name": @"CJ",
                @"age":@(10+i)
            };
            [p setValuesForKeysWithDictionary:dict];
            [persons addObject:p];
        }
        float avg = [[persons valueForKeyPath:@"@avg.age"] floatValue];
        NSLog(@"平均年纪%f", avg);
        int count = [[persons valueForKeyPath:@"@count.age"] intValue];
        NSLog(@"查询人口%d", count);
        int sum = [[persons valueForKeyPath:@"@sum.age"] intValue];
        NSLog(@"年纪总和%d", sum);
        int max = [[persons valueForKeyPath:@"@max.age"] intValue];
        NSLog(@"最大年纪%d", max);
        int min = [[persons valueForKeyPath:@"@min.age"] intValue];
        NSLog(@"最小年纪%d", min);
    }
    return 0;
}
  • 打印成果:
2023-04-18 14:27:07.295248+0800 KCObjcBuild[48112:1611583] 平均年纪14.500000
2023-04-18 14:27:07.295576+0800 KCObjcBuild[48112:1611583] 查询人口10
2023-04-18 14:27:10.179730+0800 KCObjcBuild[48112:1611583] 年纪总和145
2023-04-18 14:27:10.180148+0800 KCObjcBuild[48112:1611583] 最大年纪19
2023-04-18 14:27:10.180513+0800 KCObjcBuild[48112:1611583] 最小年纪10

5. 层层嵌套

经过forKeyPath实例变量进行取值赋值

@interface CJPet : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation CJPet
@end
@interface CJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) CJPet *pet;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
        CJPet *pet = [CJPet alloc];
        pet.name = @"Tom";
        pet.age = 2;
        person.pet = pet;
        [person setValue:@"Janry" forKeyPath:@"pet.name"];
        [person setValue:@"3" forKeyPath:@"pet.age"];
        NSLog(@"pet's name is %@ , age is %@", [person valueForKeyPath:@"pet.name"], [person valueForKeyPath:@"pet.age"]);
    }
    return 0;
}
  • 打印成果:
2023-04-18 14:37:51.278295+0800 KCObjcBuild[48404:1622414] pet's name is Janry , age is 3

三、KVC底层原理

由于NSKeyValueCoding的完结在Foundation结构,但它又不开源,咱们只能经过KVC官方文档与GNUstep源码来了解它。

1. 设值进程

官方文档上对Setter办法的进程进行了这样一段讲解

OC底层原理(十五)KVC与KVO

  • ①. 按set<Key>:_set<Key>:次序查找目标中是否有对应的办法

    • 找到了直接调用设值

    • 没有找到跳转第2步

  • ②. 判别accessInstanceVariablesDirectly成果

    • YES时依照_<key>_is<Key><key>is<Key>的次序查找成员变量,找到了就赋值;找不到就跳转第3步

    • NO时跳转第3步

  • ③. 调用setValue: forUndefinedKey:。默许状况下会引发一个反常,可是承继于NSObject的子类能够重写该办法就能够防止溃散并做出相应措施。

OC底层原理(十五)KVC与KVO

2. 取值进程

同样的官方文档上也给出了Getter办法的进程

OC底层原理(十五)KVC与KVO

  • ①. 依照get<Key><key>is<Key>_<key>次序查找目标中是否有对应的办法

    • 假如有则调用getter,履行第⑤步

    • 假如没有找到,跳转到第②步

  • ②. 查找是否有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:)

    • 假如没有找到,跳转到第③步

  • ③. 查找名为countOf<Key>enumeratorOf<Key>memberOf<Key>这三个办法(对应于NSSet类界说的原始办法)

    • 假如找到这三个办法,则创建一个呼应一切NSSet办法的署理调集目标,并回来该目标

    • 假如没有找到,跳转到第④步

  • ④. 判别accessInstanceVariablesDirectly

    • YES时依照_<key>_is<Key><key>is<Key>的次序查找成员变量,找到了就取值

    • NO时跳转第⑥步

  • ⑤. 判别取出的特点值

    • 特点值是目标,直接回来

    • 特点值不是目标,可是能够转化为NSNumber类型,则将特点值转化为NSNumber 类型回来

    • 特点值不是目标,也不能转化为NSNumber类型,则将特点值转化为NSValue类型回来

  • ⑥. 调用valueForUndefinedKey:。默许状况下会引发一个反常,可是承继于NSObject的子类能够重写该办法就能够防止溃散并做出相应措施。

    OC底层原理(十五)KVC与KVO

四、自界说KVC

依据KVC设值取值进程,咱们能够自界说KVCsetter办法和getter办法,可是这一切都是依据官方文档做出的猜测,自界说KVC只能在一定程度上取代体系KVC,大致流程简直一致:完结了 setValue:forUndefinedKey:valueForUndefinedKey: 的调用,且 accessInstanceVariablesDirectly不管为true或为false,都能坚持两次调用。

  • 新建一个NSObject+CJKVC的分类,在NSObject_CJKVC.h声明两个办法,NSObject_CJKVC.m引入#import <objc/runtime.h>
@interface NSObject (CJKVC)
- (void)cj_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)cj_valueForKey:(NSString *)key;
@end

1. 封装的办法

这儿简略封装了几个用到的办法

①. cj_performSelectorWithMethodName:value:key:安全调用办法及传两个参数:

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

②. performSelector安全调用办法及传参

- (id)cj_performSelector:(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;
}

③. getIvarListName取成员变量

- (NSMutableArray *)cj_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;
}

2. 自界说setter办法

①. 非空判别


- (void)cj_setValue:(nullable id)value forKey:(NSString *)key
{
  if (key == nil || key.length == 0) {
    return;
  }

②. 找到相关办法set<Key>_set<Key>setIs<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 cj_performSelectorWithMethodName:setKey value:value]) {
    NSLog(@"*********%@**********",setKey);
    return;
  } else if ([self cj_performSelectorWithMethodName:_setKey value:value]) {
    NSLog(@"*********%@**********",_setKey);
    return;
  } else if ([self cj_performSelectorWithMethodName:setIsKey value:value]) {
    NSLog(@"*********%@**********",setIsKey);
    return;
  }

③. 判别是否能够直接赋值实例变量,不能的状况下就调用setValue:forUndefinedKey:或抛出反常

    NSString *undefinedMethodName = @"setValue:forUndefinedKey:";
    IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));
    if (![self.class accessInstanceVariablesDirectly]) {
        if (undefinedIMP) {
            [self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
        } else {
            @throw [NSException exceptionWithName:@"CJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
        }
        return;
    }

④. 找相关实例变量进行赋值

    NSMutableArray *mArray = [self cj_getIvarListName];
    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);
        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;
    }
    @throw [NSException exceptionWithName:@"CJUnknownKeyException"
                                   reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)]
                                 userInfo:nil];
}

⑤. 调用setValue:forUndefinedKey:或抛出反常

    if (undefinedIMP) {
        [self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
    } else {
        @throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
    }
  • 总结:

    在这儿笔者存在一个疑问:没有完结setValue:forUndefinedKey:时,当时类能够呼应respondsToSelector这个办法,可是直接performSelector会溃散,所以改用了判别IMP是否为空?

3. 自界说getter办法

①. 非空判别

- (nullable id)cj_valueForKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return nil;
    }

②. 找相关办法get<Key><key>,找到就回来(这儿运用-Warc-performSelector-leaks消除警告)

    NSString *Key = key.capitalizedString;
    NSString *getKey = [NSString stringWithFormat:@"get%@",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)];
    }

③. 对NSArray进行操作:查找countOf<Key>objectIn<Key>AtIndex办法

    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(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

④. 判别是否能够直接赋值实例变量,不能的状况下就调用valueForUndefinedKey:或抛出反常

    NSString *undefinedMethodName = @"valueForUndefinedKey:";
    IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));
    if (![self.class accessInstanceVariablesDirectly]) {
        if (undefinedIMP) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
    #pragma clang diagnostic pop
        } else {
            @throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
        }
    }

⑤. 找相关实例变量,找到了就回来

    NSMutableArray *mArray = [self cj_getIvarListName];
    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);
    }

⑥. 调用valueForUndefinedKey:或抛出反常

    if (undefinedIMP) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
    #pragma clang diagnostic pop
    } else {
        @throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
    }
 }

五、KVC反常小技巧

1. 技巧一: 主动转化类型

①. 用int类型赋值会主动转成__NSCFNumber

  • 测验代码:
@interface CJPerson: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
       [person setValue: @"18" forKey: @"age"];
NSLog(@"%@---%@",[person valueForKey:@"age"], [[person valueForKey:@"age"] class]);
    }
    return 0;
}
  • lldb打印:
2023-04-23 00:42:07.826404+0800 KCObjcBuild[11565:3721417] 18---__NSCFNumber

②. 用结构体类型类型赋值会主动转成NSConcreteValue

  • 测验代码:
typedef struct {
    float x,y,z;
} TreeFloats;
@interface CJPerson: NSObject
@property (nonatomic, assign) TreeFloats threeFloats;
@end
@implementation CJPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
        TreeFloats floats = {180.0, 140.0, 18.0};
        NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(TreeFloats)];
        [person setValue:value forKey:@"threeFloats"];
        NSLog(@"%@---%@", [person valueForKey:@"threeFloats"], [[person valueForKey:@"threeFloats"] class]);
    }
    return 0;
}
  • lldb打印:
2023-04-23 00:46:14.631647+0800 KCObjcBuild[11647:3725039] {length = 12, bytes = 0x0000344300000c4300009041}---NSConcreteValue

2. 技巧二: 设置空值

有时分在设值时设置空值,能够经过重写setNilValueForKey来监听,可是以下代码只有打印一次。

  • 测验代码:
@interface CJPet: NSObject
@end
@implementation CJPet
@end
@interface CJPerson: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) CJPet *pet;
@end
@implementation CJPerson
- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"设置 %@ 是空值",key);
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
        // Int类型设置nil
        [person setValue: nil forKey: @"age"];
        // 自界说目标类型设置nil
        [person setValue: nil forKey: @"pet"];
        // NSString类型设置nil
        [person setValue: nil forKey: @"name"];
    }
    return 0;
}
  • lldb:
2023-04-23 01:02:01.328624+0800 KCObjcBuild[11937:3734452] 设置 age 是空值
  • 总结:

    这是由于setNilValueForKey只对NSNumber类型有用

    OC底层原理(十五)KVC与KVO

3. 技巧三:未界说的key

关于未界说的key咱们能够经过重写setValue:forUndefinedKey:valueForUndefinedKey:来监听

  • 测验代码:
@interface CJPerson: NSObject
@end
@implementation CJPerson
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"未界说的key--%@", key);
}
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"未界说的key--%@", key);
    return  @"未界说的key";
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
        [person setValue: @"undefined" forKey:@"undefined"];
    NSLog(@"未界说--%@", [person valueForKey:@"undefined"]);
    }
    return 0;
}
  • lldb打印:
2023-04-23 01:23:46.011104+0800 KCObjcBuild[12381:3750804] 未界说的key--undefined
2023-04-23 01:23:48.015583+0800 KCObjcBuild[12381:3750804] 未界说的key--undefined
2023-04-23 01:23:48.015794+0800 KCObjcBuild[12381:3750804] 未界说--未界说的key

4. 技巧四:键值验证

一个比较鸡肋的功用——键值验证,能够自行打开做重定向

@interface CJPerson: NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation CJPerson
- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError {
    if ([inKey isEqualToString:@"name"]) {
        [self setValue:[NSString stringWithFormat:@"里边修正一下: %@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的特点", inKey, self] code:10088 userInfo:nil];
    return NO;
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJPerson *person = [CJPerson alloc];
        NSError *error;
        NSString *name = @"CJ";
        if (![person validateValue:&name forKey:@"names" error:&error]) {
            NSLog(@"%@", error);
        } else {
            NSLog(@"%@", [person valueForKey:@"names"]);
        }
        if (![person validateValue:&name forKey:@"name" error:&error]) {
            NSLog(@"%@", error);
        } else {
            NSLog(@"%@", [person valueForKey:@"name"]);
        }
    }
    return 0;
}
  • lldb:
2023-04-23 01:38:54.756471+0800 KCObjcBuild[12710:3762990] Error Domain=names 不是 <CJPerson: 0x60000170c780> 的特点 Code=10088 "(null)"
2023-04-23 01:38:54.758050+0800 KCObjcBuild[12710:3762990] 里边修正一下: CJ

KVO

一、KVO的初探

KVO(Key-Value Observing)是苹果供给的一套事情告诉机制,这种机制允许将其他目标的特定特点的更改告诉给目标。iOS开发者能够运用KVO来检测目标特点的改动、快速做出呼应,这能够为咱们在开发强交互、呼应式应用以及完结视图和模型的双向绑定时供给大量的帮助。

在Documentation Archieve中说到一句想要了解KVO,必须先了解KVC,由于键值调查是建立在键值编码的基础上。

KVONSNotificatioCenter都是iOS调查者模式的一种完结,两者的差异在于:

  • 相关于被调查者和调查者之间的联系,KVO1对1的,NSNotificatioCenter一对多的。

  • KVO对被监听目标无侵入性,不需求修正其内部代码即可完结监听。

二、KVO的运用及留意点

1.根本运用

KVO运用三部曲:

①. 注册调查者:

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];

②. 完结回调:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}

③. 移除调查者:

[self.person removeObserver:self forKeyPath:@"name"];

2.context的运用

Key-Value Observing Programming Guide是这么描述context的.

音讯中的上下文指针包括恣意数据,这些数据将在相应的更改告诉中传递回调查者;您能够指定NULL并彻底依靠键路径字符串来确认更改告诉的来源,可是这种办法可能会导致目标的父类由于不同的原因而调查到相同的键路径,因而可能会呈现问题;一种更安全,更可扩展的办法是运用上下文确保您收到的告诉是发给调查者的,而不是超类的。

这儿提出一个设想,假如父类中有个name特点,子类中也有个name特点,两者都注册对name的调查,那么仅经过keyPath现已区别不了是哪个name发生改动了,现有两个解决办法:

  • ①. 多加一层判别——判别object,显然为了满意事务需求而去增加逻辑判别是不可取的

  • ②. 运用context传递信息,更安全、更可扩展

context运用总结:

  • ①. 不运用context作为调查值:
// context是 void * 类型,应该填 NULL 而不是 nil
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
  • ②. 运用context传递信息:
static void *PersonNameContext = &PersonNameContext;
static void *ChildNameContext = &ChildNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:ChildNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonNameContext) {
        NSLog(@"%@", change);
    } else if (context == ChildNameContext) {
        NSLog(@"%@", change);
    }
}

3. 移除告诉的必要性

也许在日常开发中你觉得是否移除告诉都无关痛痒,可是不移除会带来潜在的隐患

①. 以下是一段没有移除调查者的代码,页面push前后、键值改动前后都很正常

// CJPerson单例
@interface CJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
+ (instancetype)sharedInstance;
@end
@implementation CJPerson
+ (instancetype)sharedInstance {
    static dispatch_once_t one_t;
    static CJPerson *shared;
    dispatch_once(&one_t, ^{
        shared = [[CJPerson alloc] init];
    });
    return shared;
}
@end
// ViewController默许榜首视图
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    DeViewController *dv = [[DeViewController alloc]init];
    [self presentViewController:dv animated:true completion:nil];
}
@end
// DeViewController第二视图
@interface DeViewController ()
@property (nonatomic, strong) CJPerson *person;
@end
@implementation DeViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.redColor;
    self.person = [CJPerson sharedInstance];
    self.person.name = @"Boom";
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"+%@", self.person.name];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}
//- (void)dealloc {
//    [self.person removeObserver:self forKeyPath:@"name"];
//}
@end

②. 但当把CJPerson单例的办法创建后,pop回上一页再次push进来程序就溃散了。

OC底层原理(十五)KVC与KVO

这是由于没有移除调查,单例目标仍旧存在,再次进来时就会报出野指针过错

③. 移除了调查者之后便不会发生这种状况了——移除调查者是必要的

OC底层原理(十五)KVC与KVO

  • 总结:

    苹果官方引荐的办法是——在init的时分进行addObserver,在deallocremoveObserver,这样能够保证addremove是成对呈现的,这是一种比较理想的运用办法

4. 手动触发键值调查

有时分事务需求需求调查某个特点值,一会儿要调查了,一会又不要调查了…。假如把KVO三部曲全体去掉、再全体添上,必然又是一顿繁琐而又不必要的作业,好在KVO中有两种办法能够手动触发键值调查

  • ①. 将被调查者的automaticallyNotifiesObserversForKey回来NO(能够只对某个特点设置)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
   if ([key isEqualToString:@"name"]) {
       return NO;
   }
   return [super automaticallyNotifiesObserversForKey:key];
}
+ (BOOL)automaticallyNotifiesObserversOfName {
    return NO;
}
  • ②. 运用willChangeValueForKeydidChangeValueForKey重写被调查者的特点的setter办法

    这两个办法用于告诉体系该key的特点值即将和现已变更了

    - (void)setName:(NSString *)name {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
  • 总结:

    1. 两种办法运用的排列组合如下,能够自由组合怎么运用
    状况 回调次数
    正常状况 1
    automaticallyNotifiesObserversForKey为NO 0
    automaticallyNotifiesObserversForKey为NO且增加willChangeValueForKey、didChangeValueForKey 1
    automaticallyNotifiesObserversForKey为YES且增加willChangeValueForKey、didChangeValueForKey 2
    1. 最近发现[self willChangeValueForKey:name]和[self willChangeValueForKey:”name”]两种写法是不同的成果:重写setter办法取特点值操作不会额定发送告诉;而运用“name”会额定发送一次告诉

5. 键值调查多对一

比如有一个下载使命的需求,依据总下载量Total当时已下载量Current来得到当时下载进展Process,这个需求就有两种完结:

  • ①. 别离调查总下载量Total当时已下载量Current两个特点,其间一个特点发生改动时计算求值当时下载进展Process

  • ②. 完结keyPathsForValuesAffectingValueForKey办法,并调查process特点

只要总下载量Total当时已下载量Current恣意发生改动,keyPaths=process就能收到监听回调

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"process"]) {
        NSArray *affectingKeys = @[@"total", @"current"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
     }
     return keyPaths;
}

但仅仅是这样还不够——这样只能监听到回调,但还没有完结Process赋值——需求重写getter办法

- (NSString *)process {
    if (self.total == 0) {
        return @"0";
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.current/self.total];
}

6. 可变数组

假如CJPerson下有一个可变数组dataArray,现调查之,问点击屏幕是否打印?

@interface CJPerson : NSObject
@property (nonatomic, strong) NSMutableArray *dataArray;
@end
@implementation CJPerson
@end
@interface FirstViewController ()
@property (nonatomic, strong) CJPerson *person;
@end
@implementation FirstViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.redColor;
    self.person = [CJPerson sharedInsatnce];
    [self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person.dataArray addObject:@"CJ"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}
@end

答:不会

  • 剖析:

    • KVO是建立在KVC的基础上的,而可变数组直接增加是不会调用Setter办法

    • 可变数组dataArray没有初始化,直接增加会报错

// 初始化可变数组
self.person.dataArray = @[].mutableCopy;
// 调用setter办法
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"CJ"];

三、KVO原理(isa-swizzling

1.官方解说

Key-Value Observing Programming Guide中有一段底层完结原理的叙说:

  • ①. KVO是运用isa-swizzling技能完结的。

  • ②. 望文生义,isa指针指向保护分配表的目标的类,该分派表实质上包括指向该类完结的办法的指针以及其他数据。(这就是isa指针效果的阐明)

  • ③. 在为目标的特点注册调查者时,将修正调查目标isa指针,指向中心类而不是实在类。isa指针的值不一定反映实例的实际类。(杀人诛心,一下子把之前类的内存结构剖析给否了)

  • ④. 您永久不应依靠isa指针来确认类成员身份。相反,您应该运用class办法来确认目标实例的类。

2. 代码探究

苹果官方的坑爹阐明,没办法只能实践出真理。

①. 注册调查者之前:类目标为CJPerson,实例目标isa指向CJPerson

(lldb) po [CJPerson class]
CJPerson
(lldb) po object_getClassName(self.person)
"CJPerson"

②. 注册调查者之后:类目标为CJPerson,实例目标isa指向NSKVONotifying_CJPerson

(lldb) po [CJPerson class]
CJPerson
(lldb) po object_getClassName(self.person)
"NSKVONotifying_CJPerson"
(lldb) 
  • 定论:

    调查者注册前后CJPerson类没发生改动,但实例目标的isa指向发生改动

③. 那么这个动态生成的中心类NSKVONotifying_CJPersonCJPerson是什么联系呢?

在注册调查者前后别离调用打印子类的办法——发现NSKVONotifying_CJPersonCJPerson的子类

2023-04-26 01:38:41.812170+0800 testKVO[22488:5867933] classes = (
    CJPerson
)
2023-04-26 01:38:53.511740+0800 testKVO[22488:5867933] classes = (
    CJPerson,
    "NSKVONotifying_CJPerson"
)

OC底层原理(十五)KVC与KVO

3.动态子类探究

①. 首先得理解动态子类调查的是什么?下面调查特点变量name成员变量nickname来找差异

两个变量同时发生改动,但只有特点变量监听到回调——阐明动态子类调查的是setter办法。

OC底层原理(十五)KVC与KVO

②. 经过runtime-API打印一下动态子类和调查类的办法

- (void)printClassAllMethod:(Class)cls {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

OC底层原理(十五)KVC与KVO

  • 经过打印能够看出:
  • FXPerson类中的办法没有改动(imp完结地址没有改动)

  • NSKVONotifying_FXPerson类中重写了父类FXPersondealloc办法

  • NSKVONotifying_FXPerson类中重写了基类NSObjectclass办法和_isKVOA办法

    • 重写的class办法能够指回FXPerson类
  • NSKVONotifying_FXPerson类中重写了父类FXPersonsetName办法

    • 由于子类只承继、不重写是不会有办法imp的,调用办法时会问父类要办法完结
    • 且两个setName的地址指针不一样
    • 每调查一个特点变量就重写一个setter办法(可自行证明)

③. dealloc之后isa指向谁?——指回原类

OC底层原理(十五)KVC与KVO

④. dealloc之后动态子类会毁掉吗?——不会

页面pop后再次push进来打印FXPerson类,子类NSKVONotifying_FXPerson类仍旧存在

OC底层原理(十五)KVC与KVO

automaticallyNotifiesObserversForKey是否会影响动态子类生成——会

动态子类会依据调查特点的automaticallyNotifiesObserversForKey的布尔值来决定是否生成

4. 总结

  1. automaticallyNotifiesObserversForKeyYES时注册调查特点会生成动态子类NSKVONotifying_XXX

  2. 动态子类调查的是setter办法

  3. 动态子类重写了调查特点的setter办法、deallocclass_isKVOA办法

    • setter办法用于调查键值
    • dealloc办法用于开释时对isa指向进行操作
    • class办法用于指回动态子类的父类
    • _isKVOA用来标识是否是在调查者状态的一个标志位
  4. dealloc之后isa指向元类

  5. dealloc之后动态子类不会毁掉

四、自界说KVO

依据KVO的官方文档和上述定论,咱们将自界说KVO——下面的自界说会有runtime-API的运用和接口规划思路的讲解,最终的自界说KVO能满意根本运用的需求但仍不完善。

新建一个NSObject+CJKVO的分类,开放注册调查者办法。

- (void)cj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    //1. 验证是否存在setter办法
    [self judgeSetterMethodFromKeyPath:keyPath];
    //2. 动态生成子类
    Class subclass_kvo = [self createChildClassWithKeyPath:keyPath];
    //3. isa指向CJKVONotifying_CJPerson
    object_setClass(self, subclass_kvo);
    //4. 保存调查者信息
    CJKVOInfo *kvoInfo = [[CJKVOInfo alloc]initWithObserver:observer keyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(CJKVOAssociatedObjectKey));
    if (!observerArr) {
        observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:kvoInfo];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(CJKVOAssociatedObjectKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}

1. 验证是否存在setter办法

判别当时调查值keypath是否存在setter办法是否存在?

一开始想的是判别特点是否存在?虽然父类的特点不会对子类造成影响,可是分类中的特点虽然没有setter办法,可是会增加到propertiList中去——最终改为去判别setter办法。

#pragma mark - 验证是否存在setter办法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
    Class superClass = object_getClass(self);
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"没有当时%@的setter", keyPath]
                                     userInfo:nil];
    }
}
#pragma mark - 从get办法获取set办法的称号 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter) {
    if (getter.length <= 0) { return nil; }
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    return [NSString stringWithFormat:@"set%@%@:",firstString, leaveString];
}

2. 动态生成子类

增加class办法指向原先的类

#pragma mark - 创建子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@", CJKVONotifyingKey, oldClassName];
    Class newClass = NSClassFromString(newClassName);
    if (newClass) {
        return newClass;;
    }
    // 1. 请求类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2. 注册类
    objc_registerClassPair(newClass);
    // 2.1 增加class办法:指向CJPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)cj_kvo_class, classTypes);
    // 2.2 增加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)cj_kvo_setter, setterTypes);
    // 3.增加delloc办法
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)cj_kvo_dealloc, deallocTypes);
    return newClass;
}

①. 增加class办法,更改isa指向

Class cj_kvo_class(id self) {
    return class_getSuperclass(object_getClass(self));
}

②. 增加setter办法并回调

void cj_kvo_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"来了:%@", newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    void (*cj_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    cj_msgSendSuper(&superStruct, _cmd, newValue);
    //1. 拿到调查者
    NSMutableArray *kvoInfos = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(CJKVOAssociatedObjectKey));
    for (CJKVOInfo *info in kvoInfos) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 对新旧值进行处理
                if (info.options & NSKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & NSKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                //2. 音讯发送给调查者
                SEL observerSEL = @selector(cj_observerValueForKeyPath:ofObject:change:context:);
                ((void (*)(id, SEL, NSString*, id, NSDictionary<NSKeyValueChangeKey, id> *, void *))objc_msgSend)(info.obsever, observerSEL, keyPath, self, change, NULL);
            });
        }
    }
}

③. 毁掉调查者,往动态子类增加dealloc办法

由于页面开释时会开释持有的目标,目标开释时会调用dealloc,现在往动态子类的dealloc办法名中增加完结将isa指回去,从而在开释时就不会去找父类要办法完结

void cj_kvo_dealloc(id self) {
    object_setClass(self, cj_kvo_class(self));
}
  • 取出基类NSObjectdealloc完结与cj_dealloc进行办法交流
  • isa指回去之后持续调用真实的dealloc进行开释
  • 之所以不在+load办法中进行交流,一是由于功率低,二是由于会影响到一切类.

3. isa重指向

isa重指向——使目标的isa的值指向动态子类

    object_setClass(self, newClass);

4. 保存调查者信息

由于可能会调查多个特点值,所以以特点值-模型的办法一一保存在数组中

@interface CJKVOInfo : NSObject
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, weak) id obsever;
@property (nonatomic) NSKeyValueObservingOptions options;
- (instancetype)initWithObserver:(id)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options;
@end    
@implementation CJKVOInfo
- (instancetype)initWithObserver:(id)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options {
    self = [super init];
    if (self) {
        self.obsever = observer;
        self.keyPath = keyPath;
        self.options = options;
    }
    return self;
}
@end