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

KVC

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

由于apple原生的Foundation.framework是不开源的,所以我们是无法通过源码学习流程的!但是有个组织GNUstep还原实现Foundation的功能,我们可以通过这部分源码了解KVCKVO原理。

一、KVC的初探

KVC的定义及API

KVC(Key-Value Coding)是利用NSKeyValueCoding,对象采用这种机制来提供对其属性的间接访问

  • 测试代码:
objectiveC 复制代码
@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常用方法,这些也是我们经常用到的:
  • 常用方法:
objectiveC 复制代码
// 通过 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类别的其它方法:
objectiveC 复制代码
// 默认为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.mmsetter方法根据修饰符不同 调用不同方法,最后统一调用reallySetProperty方法。

  • 结论:

    来到reallySetProperty再根据内存偏移量 取出属性,根据修饰符完成不同的操作。

    • ①. 在第一个属性name赋值时,此时的内存偏移量为8,刚好偏移isa所占内存(8字节)来到name
  1. 至于是哪里调用的objc_setProperty_nonatomic_copy

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

二、KVC的使用

1. 基本类型

注意一下NSInteger这类基本类型属性 赋值时要转成NSNumberNSString

objectiveC 复制代码
@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;
}
  • 打印结果:
c 复制代码
2023-04-18 12:43:48.958195+0800 KCObjcBuild[45906:1531706] person's name is CJ, the age is 12

2. 集合类型

两种方法对数组进行赋值,更推荐使用第二种方法

objectiveC 复制代码
@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(@"第二次改变:%@", [person valueForKey:@"family"]);
    }
    return 0;
}
  • 打印结果:
c 复制代码
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] 第二次改变:(
    CJSon,
    CJGrapa,
    CJFather,
    CJMonther
)

3. 访问非对象类型(结构体)

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

objectiveC 复制代码
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;
}
  • 打印结果:
c 复制代码
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
  • 测试代码:

objectiveC 复制代码
@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;
}
  • 打印结果:
c 复制代码
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实例变量 进行取值赋值

objectiveC 复制代码
@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;
}
  • 打印结果:
C 复制代码
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方法的过程进行了这样一段讲解

  • ①. 按set<Key>:_set<Key>:顺序查找对象中是否有对应的方法

    • 找到了直接调用设值

    • 没有找到跳转第2步

  • ②. 判断accessInstanceVariablesDirectly结果

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

    • NO时跳转第3步

  • ③. 调用setValue: forUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施。

2. 取值过程

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

  • ①. 按照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的子类可以重写该方法就可以避免崩溃并做出相应措施。

四、自定义KVC

根据KVC设值取值 过程,我们可以自定义KVCsetter方法和getter方法,但是这一切都是根据官方文档做出的猜测,自定义KVC只能在一定程度上取代系统KVC,大致流程几乎一致:实现了 setValue:forUndefinedKey:valueForUndefinedKey: 的调用,且 accessInstanceVariablesDirectly无论为true或为false,都能保持两次调用。

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

1. 封装的方法

这里简单封装了几个用到的方法

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

objectiveC 复制代码
- (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安全调用方法及传参

objectiveC 复制代码
- (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取成员变量

objectiveC 复制代码
- (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方法

①. 非空判断

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

②. 找到相关方法set<Key>_set<Key>setIs<Key>,若存在就直接调用

objectiveC 复制代码
    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:或抛出异常

objectiveC 复制代码
    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;
    }

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

objectiveC 复制代码
    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:或抛出异常

objectiveC 复制代码
    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方法

①. 非空判断

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

②. 找相关方法get<Key><key>,找到就返回(这里使用-Warc-performSelector-leaks消除警告)

objectiveC 复制代码
    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方法

objectiveC 复制代码
    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:或抛出异常

objectiveC 复制代码
    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];
        }
    }

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

objectiveC 复制代码
    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:或抛出异常

objectiveC 复制代码
    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

  • 测试代码:
objectiveC 复制代码
@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打印:
c 复制代码
2023-04-23 00:42:07.826404+0800 KCObjcBuild[11565:3721417] 18---__NSCFNumber

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

  • 测试代码:
objectiveC 复制代码
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打印:
c 复制代码
2023-04-23 00:46:14.631647+0800 KCObjcBuild[11647:3725039] {length = 12, bytes = 0x0000344300000c4300009041}---NSConcreteValue

2. 技巧二: 设置空值

有时候在设值时设置空值 ,可以通过重写setNilValueForKey来监听,但是以下代码只有打印一次。

  • 测试代码:
objectiveC 复制代码
@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:
c 复制代码
2023-04-23 01:02:01.328624+0800 KCObjcBuild[11937:3734452] 设置 age 是空值
  • 总结:

    这是因为setNilValueForKey只对NSNumber类型有效

3. 技巧三:未定义的key

对于未定义的key我们可以通过重写setValue:forUndefinedKey:valueForUndefinedKey:来监听

  • 测试代码:
objectiveC 复制代码
@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打印:
c 复制代码
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. 技巧四:键值验证

一个比较鸡肋的功能------键值验证,可以自行展开做重定向

objectiveC 复制代码
@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:
c 复制代码
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观察者模式的一种实现,两者的区别在于:

  • 相对于被观察者和观察者之间的关系,KVO一对一 的,NSNotificatioCenter一对多的。

  • KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

二、KVO的使用及注意点

1.基本使用

KVO使用三部曲:

①. 注册观察者:

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

②. 实现回调:

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

③. 移除观察者:

objectiveC 复制代码
[self.person removeObserver:self forKeyPath:@"name"];

2.context的使用

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

消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传递回观察者;您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因而观察到相同的键路径,因此可能会出现问题;一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。

这里提出一个假想,如果父类中有个name属性,子类中也有个name属性,两者都注册对name的观察,那么仅通过keyPath已经区分不了是哪个name发生变化了,现有两个解决办法:

  • ①. 多加一层判断------判断object,显然为了满足业务需求而去增加逻辑判断是不可取的

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

context使用总结:

  • ①. 不使用context作为观察值:
objectiveC 复制代码
// context是 void * 类型,应该填 NULL 而不是 nil
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
  • ②. 使用context传递信息:
objectiveC 复制代码
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前后、键值改变前后都很正常

objectiveC 复制代码
// 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进来程序就崩溃了。

这是因为没有移除观察,单例对象依旧存在,再次进来时就会报出野指针错误

③. 移除了观察者之后便不会发生这种情况了------移除观察者是必要的

  • 总结:

    苹果官方推荐的方式是------在init的时候进行addObserver,在deallocremoveObserver,这样可以保证addremove是成对出现的,这是一种比较理想的使用方式

4. 手动触发键值观察

有时候业务需求需要观察某个属性值,一会儿要观察了,一会又不要观察了...。如果把KVO三部曲整体去掉、再整体添上,必然又是一顿繁琐而又不必要的工作,好在KVO中有两种办法可以手动触发键值观察

  • ①. 将被观察者的automaticallyNotifiesObserversForKey返回NO(可以只对某个属性设置)
objectiveC 复制代码
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
   if ([key isEqualToString:@"name"]) {
       return NO;
   }
   return [super automaticallyNotifiesObserversForKey:key];
}

+ (BOOL)automaticallyNotifiesObserversOfName {
    return NO;
}
  • ②. 使用willChangeValueForKeydidChangeValueForKey重写被观察者的属性的setter方法

    这两个方法用于通知系统该key的属性值即将和已经变更了

objectiveC 复制代码
    - (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就能收到监听回调

objectiveC 复制代码
+ (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方法

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

6. 可变数组

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

objectiveC 复制代码
@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没有初始化,直接添加会报错

objectiveC 复制代码
// 初始化可变数组
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

c 复制代码
(lldb) po [CJPerson class]
CJPerson

(lldb) po object_getClassName(self.person)
"CJPerson"

②. 注册观察者之后:类对象为CJPerson,实例对象isa指向NSKVONotifying_CJPerson

C 复制代码
(lldb) po [CJPerson class]
CJPerson

(lldb) po object_getClassName(self.person)
"NSKVONotifying_CJPerson"

(lldb) 
  • 结论:

    观察者注册前后CJPerson类没发生变化,但实例对象的isa指向发生变化

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

在注册观察者前后分别调用打印子类的方法------发现NSKVONotifying_CJPersonCJPerson的子类

C 复制代码
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"
)

3.动态子类探索

①. 首先得明白动态子类观察 的是什么?下面观察属性变量 name成员变量 nickname来找区别

两个变量同时发生变化,但只有属性变量监听到回调------说明动态子类观察 的是setter方法。

②. 通过runtime-API打印一下动态子类和观察类的方法

objectiveC 复制代码
- (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);
}
  • 通过打印可以看出:
  • FXPerson类中的方法没有改变(imp实现地址没有变化)

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

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

    • 重写的class方法可以指回FXPerson类
  • NSKVONotifying_FXPerson类中重写了父类FXPersonsetName方法

    • 因为子类只继承、不重写是不会有方法imp的,调用方法时会问父类要方法实现
    • 且两个setName的地址指针不一样
    • 每观察一个属性变量就重写一个setter方法(可自行论证)

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

④. dealloc之后动态子类会销毁吗?------不会

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

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的分类,开放注册观察者方法。

objectiveC 复制代码
- (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方法。

objectiveC 复制代码
#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方法指向原先的类

objectiveC 复制代码
#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指向

objectiveC 复制代码
Class cj_kvo_class(id self) {
    return class_getSuperclass(object_getClass(self));
}

②. 添加setter方法并回调

objectiveC 复制代码
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指回去,从而在释放时就不会去找父类要方法实现

objectiveC 复制代码
void cj_kvo_dealloc(id self) {
    object_setClass(self, cj_kvo_class(self));
}
  • 取出基类NSObjectdealloc实现与cj_dealloc进行方法交换
  • isa指回去之后继续调用真正的dealloc进行释放
  • 之所以不在+load方法中进行交换,一是因为效率低,二是因为会影响到所有类.

3. isa重指向

isa重指向------使对象的isa的值指向动态子类

objectiveC 复制代码
    object_setClass(self, newClass);

4. 保存观察者信息

由于可能会观察多个属性值,所以以属性值-模型的形式一一保存在数组中

objectiveC 复制代码
@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
相关推荐
诺离3 小时前
给iPhone实现投屏控制,无需越狱
ios·iphone
️ 邪神4 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】启动页
android·flutter·ios·鸿蒙·reactnative
hunteritself8 小时前
谷歌Gemini发布iOS版App,live语音聊天免费用!
人工智能·ios·chatgpt·openai·语音识别
解压专家6669 小时前
7z 解压器手机版与解压专家:安卓解压工具对决
ios·智能手机·winrar·7-zip
袁代码10 小时前
SwiftUI开发教程系列 - 第十二章:本地化与多语言支持
开发语言·前端·ios·swiftui·swift·ios开发
️ 邪神11 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】水平布局
flutter·ios·鸿蒙·reactnative·anroid
_可乐无糖11 小时前
iOS UI自动化 Appium的元素定位方式及比较
ios·appium·自动化
软件聚导航15 小时前
uniapp 实现 ble蓝牙同时连接多台蓝牙设备,支持app、苹果(ios)和安卓手机,以及ios连接蓝牙后的一些坑
android·ios·uni-app
q567315231 天前
用 PHP或Python加密字符串,用iOS解密
java·python·ios·缓存·php·命令模式
Hgc558886661 天前
iOS 18.2 重磅更新:6个大动作
ios