KVC
在咱们的往常开发中经常用到KVC
赋值取值、字典转模型,但KVC
的底层原理又是怎样的呢?
由于apple原生的Foundation.framework
是不开源的,所以咱们是无法经过源码学习流程的!可是有个安排GNUstep复原完结Foundation
的功用,咱们能够经过这部分源码了解KVC
与KVO
原理。
一、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
-
②.
NSArray
、NSDictionary
、NSMutableDictionary
、NSOrderedSet
、NSSet
等也遵守KVC
协议 -
③. 除少数类型(结构体)以外都能够运用
KVC
-
- 在gnustep源码中能够看到
NSObject(KeyValueCoding)
中的KVC
常用办法,这些也是咱们经常用到的:
- 常用办法:
// 经过 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;
主动生成的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
@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
办法的进程进行了这样一段讲解
-
①. 按
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>
。
@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
类型有用
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
,由于键值调查是建立在键值编码的基础上。
而KVO
和NSNotificatioCenter
都是iOS
调查者模式的一种完结,两者的差异在于:
-
相关于被调查者和调查者之间的联系,
KVO
是1对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
进来程序就溃散了。
这是由于没有移除调查,单例目标
仍旧存在,再次进来时就会报出野指针过错
了
③. 移除了调查者之后便不会发生这种状况了——移除调查者是必要的
-
总结:
苹果官方引荐的办法是——在
init
的时分进行addObserver
,在dealloc
时removeObserver
,这样能够保证add
和remove
是成对呈现的,这是一种比较理想的运用办法
4. 手动触发键值调查
有时分事务需求需求调查某个特点值,一会儿要调查了,一会又不要调查了…。假如把KVO
三部曲全体去掉、再全体添上,必然又是一顿繁琐而又不必要的作业,好在KVO
中有两种办法能够手动触发键值调查:
- ①. 将被调查者的
automaticallyNotifiesObserversForKey
回来NO
(能够只对某个特点设置)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
-
②. 运用
willChangeValueForKey
、didChangeValueForKey
重写被调查者的特点的setter
办法这两个办法用于告诉体系该
key
的特点值即将和现已变更了
- (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
就能收到监听回调
+ (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_CJPerson
和CJPerson
是什么联系呢?
在注册调查者前后别离调用打印子类的办法——发现NSKVONotifying_CJPerson
是CJPerson
的子类
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
打印一下动态子类和调查类的办法
- (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
的分类,开放注册调查者办法。
- (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));
}
- 取出基类
NSObject
的dealloc
完结与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