KVC
在我们的平常开发中经常用到KVC
赋值取值 、字典转模型 ,但KVC
的底层原理又是怎样的呢?
由于apple原生的Foundation.framework
是不开源的,所以我们是无法通过源码学习流程的!但是有个组织GNUstep还原实现Foundation
的功能,我们可以通过这部分源码了解KVC
与KVO
原理。
一、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
-
②.
NSArray
、NSDictionary
、NSMutableDictionary
、NSOrderedSet
、NSSet
等也遵守KVC
协议 -
③. 除少数类型(结构体)以外都可以使用
KVC
-
- 在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;
自动生成的setter
和getter
方法
试想一下编译器 要为成千上万个属性 分别生成setter
和getter
方法那不得歇菜了嘛?
于是苹果开发者们就运用通用原则 给所有属性 都提供了同一个入口------objc-accessors.mm中setter
方法根据修饰符不同 调用不同方法,最后统一调用reallySetProperty
方法。
-
结论:
来到
reallySetProperty
再根据内存偏移量 取出属性,根据修饰符完成不同的操作。- ①. 在第一个属性
name
赋值时,此时的内存偏移量为8,刚好偏移isa
所占内存(8字节)来到name
- ①. 在第一个属性
-
至于是哪里调用的
objc_setProperty_nonatomic_copy
?并不是在
objc
源码中,而在llvm源码中发现了它,根据它一层层找上去就能找到源头。
二、KVC的使用
1. 基本类型
注意一下NSInteger
这类基本类型 的属性 赋值时要转成NSNumber
或NSString
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
的设值 与取值 过程,我们可以自定义KVC
的setter
方法和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
,因为键值观察 是建立在键值编码的基础上。
而KVO
和NSNotificatioCenter
都是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
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,这是一种比较理想的使用方式
4. 手动触发键值观察
有时候业务需求需要观察某个属性值,一会儿要观察了,一会又不要观察了...。如果把KVO
三部曲整体去掉、再整体添上,必然又是一顿繁琐而又不必要的工作,好在KVO
中有两种办法可以手动触发键值观察:
- ①. 将被观察者的
automaticallyNotifiesObserversForKey
返回NO
(可以只对某个属性设置)
objectiveC
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
-
②. 使用
willChangeValueForKey
、didChangeValueForKey
重写被观察者的属性的setter
方法这两个方法用于通知系统该
key
的属性值即将和已经变更了
objectiveC
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
-
总结:
- 两种方式使用的排列组合如下,可以自由组合如何使用
情况 回调次数 正常情况 1 automaticallyNotifiesObserversForKey为NO 0 automaticallyNotifiesObserversForKey为NO且添加willChangeValueForKey、didChangeValueForKey 1 automaticallyNotifiesObserversForKey为YES且添加willChangeValueForKey、didChangeValueForKey 2 - 最近发现[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_CJPerson
和CJPerson
是什么关系呢?
在注册观察者前后分别调用打印子类的方法------发现NSKVONotifying_CJPerson
是CJPerson
的子类
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类
中重写了父类FXPerson
的dealloc
方法 -
NSKVONotifying_FXPerson类
中重写了基类NSObject
的class
方法和_isKVOA
方法- 重写的
class
方法可以指回FXPerson类
- 重写的
-
NSKVONotifying_FXPerson类
中重写了父类FXPerson
的setName
方法- 因为子类只继承、不重写是不会有方法imp的,调用方法时会问父类要方法实现
- 且两个
setName
的地址指针不一样 - 每观察一个
属性变量
就重写一个setter
方法(可自行论证)
③. dealloc
之后isa
指向谁?------指回原类
④. dealloc
之后动态子类会销毁吗?------不会
页面pop
后再次push
进来打印FXPerson类
,子类NSKVONotifying_FXPerson类
依旧存在
⑤automaticallyNotifiesObserversForKey
是否会影响动态子类生成------会
动态子类会根据观察属性的automaticallyNotifiesObserversForKey
的布尔值来决定是否生成
4. 总结
-
automaticallyNotifiesObserversForKey
为YES
时注册观察属性会生成动态子类NSKVONotifying_XXX
-
动态子类观察的是
setter
方法 -
动态子类重写了观察属性的
setter
方法、dealloc
、class
、_isKVOA
方法setter
方法用于观察键值dealloc
方法用于释放时对isa指向进行操作class
方法用于指回动态子类的父类_isKVOA
用来标识是否是在观察者状态的一个标志位
-
dealloc
之后isa
指向元类 -
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));
}
- 取出基类
NSObject
的dealloc
实现与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