1. KVC赋值为什么能触发KVO
上一篇KVO的原理及应用遗留了一个问题:实例变量ivar,通过kvc也是可以触发kvo的,你知道为什么吗?
关于这个问题,大概画了个流程图
图片.png
- 在添加观察者,观察ivar的时候,会对支持KVO的进行setter的生成,这个setter生成之后会存储在一个全局的集合中
- 对于观察的属性则会给派生的类增加一个set方法,方法的imp是_NSSetXXXValueAndNotify
- 对于观察的ivar(没有实现set的情况),则会根据kvc的取实例变量的方式得到Ivar然后生成一个setter并缓存到集合中
- 调用setValue:forKey的时候则去查找setter对象,执行setter的imp,也就是_NSSetXXXValueAndNotify
- _NSSetXXXValueAndNotify的内部则走了kvo的流程
KVC的取值赋值的逻辑,大家都比较清楚了,这里就不多说了,今天主要是看一下设置nil系统抛出异常的场景及处理
2. KVC赋值nil异常的情况
2.1 测试代码
@interface TestKVOObject : NSObject
@property (nonatomic, assign) NSInteger testInteger;
@property (nonatomic, assign) NSRange testRange;
@end
- (void)testKVCNilValue {
TestKVOObject *test = [TestKVOObject new];
[test setValue:nil forKey:@"testInteger"];
}
执行完后发现程序闪退了
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<TestKVOObject 0x600003f939a0> setNilValueForKey]: could not set nil as the value for the key testInteger.'
程序执行了setNilValueForKey
然后闪退了;关于是怎么调用到setNilValueForKey
可以用hopper查看下伪代码,或者debug调试看看汇编,在set方法里会判断传的值是否为nil,为nil则执行的方法的地址0x7fff86b9d188
;我查看的是模拟器的Foundation.framework
的伪代码,所以我用lldb直接打印一下p (IMP)0x7fff86b9d188
,可以看到0x00007fff86b9d188 ("setNilValueForKey:")
,当对于某些类型的数据kvc赋值的时候如果value是nil就会走到setNilValueForKey
分支
void __NSSetLongValueForKeyInIvar(int arg0) {
rbx = arg0;
if (rdx != 0x0) {
*(rbx + ivar_getOffset(r8)) = (*_objc_msgSend)(rdx, *0x7fff86b9ca70);
}
else {
rdi = rbx;
(*_objc_msgSend)(rdi, *0x7fff86b9d188);
}
return;
}
void __NSSetLongValueForKeyWithMethod(int arg0) {
r14 = arg0;
if (rdx != 0x0) {
rdi = r14;
rax = method_getImplementation(r8);
(rax)(rdi, method_getName(r8), (*_objc_msgSend)(rdx, *0x7fff86b9ca70));
}
else {
rdi = r14;
(*_objc_msgSend)(rdi, *0x7fff86b9d188);
}
return;
}
(lldb) p (IMP)0x7fff86b9d188
(IMP) $2 = 0x00007fff86b9d188 ("setNilValueForKey:")
2.2 问题分析
看看方法的注释
/* Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism. The default implementation of this method raises an NSInvalidArgumentException. You can override it to map nil values to something meaningful in the context of your application.
*/
- (void)setNilValueForKey:(NSString *)key;
文档说的很清楚了,当是NSNumber标量类型或NSValue结构类型的实例,设置nil值的时候会抛出一个异常NSInvalidArgumentException
3. 如何解决
关于怎么解决这个问题,苹果文档注释已经给出了答案You can override it to map nil values to something meaningful in the context of your application
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"testInteger"]) { // fix 设置number为nil的时候导致抛出异常
[self setValue:@(0) forKey:key];
}
}
只需要在类中重写setNilValueForKey
方法,然后设置对应的key的value为有意义的值就好了,比如NSInteger的属性,我设置一个@(0)
的默认值
然而作为一个稍微有点要求的程序员,这么干显然不够优雅,有多个属性,那不是要各种硬编码去处理
3.1 重写方法覆盖所有值类型属性的异常处理
我们不可能去一个属性一个属性的去判断,去做异常的处理;可以用runtime的API去获取到key对应的Ivar或者Method,然后获取到它的encoding就能知道它是不是值类型了,是不是我们需要去处理的类型了
大致思路有了,现在开始实现:
3.1.1 根据key获取Ivar
获取Ivar的整体思路:根据KVC的取值的顺序_key、_isKey、key、isKey
来依次拼接字符串得到对应的ivarName,在调用runtime的APIclass_getInstanceVariable
来获取到Ivar
- (nullable Ivar)hc_getIvarByKey:(NSString *)key {
if (![key isKindOfClass:[NSString class]] || key.length <= 0) {
return nil;
}
// 按照_key _isKey key isKey的方式去获取ivar
NSString *firstCharacter = [key substringToIndex:1];
NSString *upperFirstKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
NSString *_keyName = [NSString stringWithFormat:@"_%@", key];
NSString *_isKeyName = [NSString stringWithFormat:@"_is%@", upperFirstKey];
NSString *keyName = key;
NSString *isKeyName = [NSString stringWithFormat:@"is%@", upperFirstKey];
Ivar ivar;
if ((ivar = [self hc_getIvarByIvarName:_keyName])
|| (ivar = [self hc_getIvarByIvarName:_isKeyName])
|| (ivar = [self hc_getIvarByIvarName:keyName])
|| (ivar = [self hc_getIvarByIvarName:isKeyName])) {
return ivar;
}
return nil;
}
- (nullable Ivar)hc_getIvarByIvarName:(NSString *)ivarNameString {
const char *ivarName = [ivarNameString cStringUsingEncoding:NSUTF8StringEncoding];
Ivar ivar = class_getInstanceVariable(self.class, ivarName);
return ivar;
}
3.1.2 根据Ivar解析encoding来判断是否是需要处理的类型
这里所说的需要处理的类型就是NSNumber标量类型或NSValue结构类型
;如何判断了,则可以根据encoding来判断;这里有2种方案可以获取到encoding信息
- @encode(type)函数一个个的打印
- 查阅官方的文档Type Encodings
关于NSNumber值类型可以参照对照表:
图片.png
关于Value类型结构体类型:
图片.png
那么我们按照文档的规则,可以推导出NSRange
的encode{_NSRange=QQ}
(lldb) po @encode(NSRange)
"{_NSRange=QQ}"
现在我们知道了需要处理的类型的encode信息,那么我们就拿已知的信息跟Ivar
的typeEncoding来比较就能判断是不是需要处理的类型的Ivar
了
获取Ivar
的typeEncoding也是用runtime的API就可以获取到const char *typeEncoding = ivar_getTypeEncoding(ivar)
例如NSRange
得到的{_NSRange=\"location\"Q\"length\"Q}
,可以看到获取到的跟@encode
获取到的差异就是后面的结构体的字段有字段名的信息。
那么我们就判断'='字符前面这一段就可以判断是不是一个类型了
至此整体的思路有了:
- 获取typeEncoding的第一个字符,判断是对照表中number类型的,则处理number的设置nil的场景,直接设置一个
@(0)
- typeEncoding[0]是字符'{'表示是value类型结构体了,这时候判断typeEncoding的'='字符前的字符是否一样就判断是否是value类型结构体了,针对结构体的设置nil的场景,则需要根据结构体的不同分别去设置
- 如果不是需要处理的类型,则调用super的
setNilValueForKey
走系统的处理逻辑
- (void)setNilValueForKey:(NSString *)key {
Ivar ivar = [self hc_getIvarByKey:key];
if (!ivar) {
[super setNilValueForKey:key];
}
const char *typeEncoding = ivar_getTypeEncoding(ivar);
switch (typeEncoding[0]) {
// NSNumber scalar type
case 'q': // longlong
case 'Q': // unsigned longlong
case 'i': // int
case 'I': // unsigned int
case 'l': // long
case 'L': // unsigned long
case 's': // short
case 'S': // unsigned short
case 'd': // double
case 'f': // float
[self setValue:@(0) forKey:key];
break;
case '{': {
char* idx = index(typeEncoding, '='); // 获取'='字符串中第一个出现的参数'=' 地址,然后将该字符出现的地址返回
/*
eg:"0x000000010bac7c7a {_NSRange=\"location\"Q\"length\"Q}" idx则为 0x000000010bac7c83 "=\"location\"Q\"length\"Q}"
Printing description of idx:
(char *) idx = 0x000000010bac7c83 "=\"location\"Q\"length\"Q}"
Printing description of typeEncoding:
(const char *) typeEncoding = 0x000000010bac7c7a "{_NSRange=\"location\"Q\"length\"Q}"
*/
if (idx == NULL) { // 如果为空则表示没有找到'=',此时走远来的流程
[super setNilValueForKey:key];
}
// 处理NSValue的一些场景:比如NSRange、CGRect、CGPoint、CGSize;也就是NSValue structure type
/*
int strncmp(const char *str1, const char *str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字节
如果返回值 < 0,则表示 str1 小于 str2。
如果返回值 > 0,则表示 str2 小于 str1。
如果返回值 = 0,则表示 str1 等于 str2。
*/
NSValue *value;
long cmpLength = idx - typeEncoding;
#define SAME_ENCODE(name) (strncmp(typeEncoding, @encode(name), cmpLength) == 0)
if (SAME_ENCODE(NSRange)) {
value = [NSValue valueWithRange:NSMakeRange(0, 0)];
} else if (SAME_ENCODE(CGPoint)) {
value = [NSValue valueWithCGPoint:CGPointZero];
} else if (SAME_ENCODE(CGSize)) {
value = [NSValue valueWithCGSize:CGSizeZero];
} else if (SAME_ENCODE(CGRect)) {
value = [NSValue valueWithCGRect:CGRectZero];
} else if (SAME_ENCODE(CGVector)) {
value = [NSValue valueWithCGVector:CGVectorMake(0, 0)];
} else if (SAME_ENCODE(UIEdgeInsets)) {
value = [NSValue valueWithUIEdgeInsets:UIEdgeInsetsZero];
} else if (SAME_ENCODE(UIOffset)) {
value = [NSValue valueWithUIOffset:UIOffsetZero];
} else if (SAME_ENCODE(CGAffineTransform)) {
value = [NSValue valueWithCGAffineTransform:CGAffineTransformIdentity];
}
#ifndef FOUNDATION_HAS_DIRECTIONAL_GEOMETRY
else if (@available(iOS 11.0, *)) {
if (SAME_ENCODE(NSDirectionalEdgeInsets)) {
value = [NSValue valueWithDirectionalEdgeInsets:NSDirectionalEdgeInsetsZero];
}
} else {
// Fallback on earlier versions
}
#endif
if (value != nil) {
[self setValue:value forKey:key];
} else {
[super setNilValueForKey:key];
}
}
break;
default:
[super setNilValueForKey:key];
break;
}
}
代码中用到了一些C函数,也是查了下文档才了解了C的字符串的一些操作函数,也简单说明一下:
index函数
char* idx = index(typeEncoding, '=');
获取'='字符串中第一个出现的参数'=' 地址,然后将该字符出现的地址返回
eg:typeEncoding为"0x000000010bac7c7a {_NSRange=\"location\"Q\"length\"Q}"
idx则为 0x000000010bac7c83 "=\"location\"Q\"length\"Q}"
Printing description of idx:
(char *) idx = 0x000000010bac7c83 "=\"location\"Q\"length\"Q}"
Printing description of typeEncoding:
(const char *) typeEncoding = 0x000000010bac7c7a "{_NSRange=\"location\"Q\"length\"Q}"
strncmp函数
int strncmp(const char *str1, const char *str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字节
如果返回值 < 0,则表示 str1 小于 str2。
如果返回值 > 0,则表示 str2 小于 str1。
如果返回值 = 0,则表示 str1 等于 str2。
获取需要比较的字符长度:
idx的地址是=
字符的地址,typeEncoding的地址是首字符的地址,两个一减就得到=
字符之前的长度了
long cmpLength = idx - typeEncoding;
判断是否是需要处理的类型:
只需要判断=
前面的字符是否一样就可以了
(strncmp(typeEncoding, @encode(name), cmpLength) == 0)
至此,已经解决了Number值类型,Value结构体类型设置nil的异常处理了。
3.2 让你的代码更健壮
上面我是在类中重写了setNilValueForKey
方法,来处理的,这有个弊端就是不能对所有的类都生效,除非你重写了基类中的实现,这样才能对所有的对象生效
显然也是有手段的,hook掉NSObject的setNilValueForKey
方法,将其实现改为上面的逻辑即可
这里我优化了一下整体的获取encoding信息的逻辑:先获取set方法的参数encoding信息,如果没有set方法,再判断是否支持
accessInstanceVariablesDirectly
再去获取Ivar信息
整体的实现思路:
- 获取set方法Method,如果有则获取方法index为2的参数的typeEncoding信息
- 如果没有set方法,判断是否支持
accessInstanceVariablesDirectly
,支持获取Ivar信息得到它的typeEncoding信息- 获取到的typeEncoding信息为空则调用原始的实现
- 比较typeEncoding跟@encode得到的信息,来确定是否是值类型,去做处理;不需要处理的情况也调用原始的实现
``
根据key获取Method:
- (nullable Method)hc_getMethodByKey:(NSString *)key {
if (![key isKindOfClass:[NSString class]] || key.length <= 0) {
return nil;
}
NSString *firstCharacter = [key substringToIndex:1];
NSString *upperFirstKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
NSString *setKeyName = [NSString stringWithFormat:@"set%@:", upperFirstKey];
NSString *_setKeyName = [NSString stringWithFormat:@"_set%@:", upperFirstKey];
NSString *setIsKeyName = [NSString stringWithFormat:@"setIs%@:", upperFirstKey];
Method method;
#define METHOD_BY_NAME(selName) class_getInstanceMethod(self.class, NSSelectorFromString(selName))
if ((method = METHOD_BY_NAME(setKeyName))
|| (method = METHOD_BY_NAME(_setKeyName))
|| (method = METHOD_BY_NAME(setIsKeyName))) {
return method;
}
return nil;
}
完整的实现代码:
@interface NSObject(HCKVCNilHandle)
@end
@implementation NSObject(HCKVCNilHandle)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originMethod = class_getInstanceMethod(self.class, @selector(setNilValueForKey:));
Method hookMethod = class_getInstanceMethod(self.class, @selector(hc_setNilValueForKey:));
method_exchangeImplementations(originMethod, hookMethod);
});
}
- (nullable Ivar)hc_getIvarByKey:(NSString *)key {
if (![key isKindOfClass:[NSString class]] || key.length <= 0) {
return nil;
}
// 按照_key _isKey key isKey的方式去获取ivar
NSString *firstCharacter = [key substringToIndex:1];
NSString *upperFirstKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
NSString *_keyName = [NSString stringWithFormat:@"_%@", key];
NSString *_isKeyName = [NSString stringWithFormat:@"_is%@", upperFirstKey];
NSString *keyName = key;
NSString *isKeyName = [NSString stringWithFormat:@"is%@", upperFirstKey];
Ivar ivar;
if ((ivar = [self hc_getIvarByIvarName:_keyName])
|| (ivar = [self hc_getIvarByIvarName:_isKeyName])
|| (ivar = [self hc_getIvarByIvarName:keyName])
|| (ivar = [self hc_getIvarByIvarName:isKeyName])) {
return ivar;
}
return nil;
}
- (nullable Ivar)hc_getIvarByIvarName:(NSString *)ivarNameString {
const char *ivarName = [ivarNameString cStringUsingEncoding:NSUTF8StringEncoding];
Ivar ivar = class_getInstanceVariable(self.class, ivarName);
return ivar;
}
- (nullable Method)hc_getMethodByKey:(NSString *)key {
if (![key isKindOfClass:[NSString class]] || key.length <= 0) {
return nil;
}
NSString *firstCharacter = [key substringToIndex:1];
NSString *upperFirstKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
NSString *setKeyName = [NSString stringWithFormat:@"set%@:", upperFirstKey];
NSString *_setKeyName = [NSString stringWithFormat:@"_set%@:", upperFirstKey];
NSString *setIsKeyName = [NSString stringWithFormat:@"setIs%@:", upperFirstKey];
Method method;
#define METHOD_BY_NAME(selName) class_getInstanceMethod(self.class, NSSelectorFromString(selName))
if ((method = METHOD_BY_NAME(setKeyName))
|| (method = METHOD_BY_NAME(_setKeyName))
|| (method = METHOD_BY_NAME(setIsKeyName))) {
return method;
}
return nil;
}
- (void)hc_setNilValueForKey:(NSString *)key {
// 获取是否有set方法
Method method = [self hc_getMethodByKey:key];
const char *typeEncoding = NULL;
if (method != nil) {
typeEncoding = method_copyArgumentType(method, 2); // 获取参数的encoding信息,method有2个缺省参数 self _cmd 所以这里是2
} else if ([self.class accessInstanceVariablesDirectly]) {
// 获取ivar
Ivar ivar = [self hc_getIvarByKey:key];
if (ivar != nil) {
typeEncoding = ivar_getTypeEncoding(ivar);
}
}
if (typeEncoding == NULL) {
[self hc_setNilValueForKey:key];
return;
}
// 遍历出所有的number、value类型的encoding,针对性的处理
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
/*
Printing description of typeEncoding:
(const char *) typeEncoding = 0x000000010f4c5c7a "{_NSRange=\"location\"Q\"length\"Q}"
(lldb) po @encode(NSRange)
"{_NSRange=QQ}"
*/
switch (typeEncoding[0]) {
// NSNumber scalar type
case 'q': // longlong
case 'Q': // unsigned longlong
case 'i': // int
case 'I': // unsigned int
case 'l': // long
case 'L': // unsigned long
case 's': // short
case 'S': // unsigned short
case 'd': // double
case 'f': // float
[self setValue:@(0) forKey:key];
break;
case '{': {
char* idx = index(typeEncoding, '='); // 获取'='字符串中第一个出现的参数'=' 地址,然后将该字符出现的地址返回
/*
eg:"0x000000010bac7c7a {_NSRange=\"location\"Q\"length\"Q}" idx则为 0x000000010bac7c83 "=\"location\"Q\"length\"Q}"
Printing description of idx:
(char *) idx = 0x000000010bac7c83 "=\"location\"Q\"length\"Q}"
Printing description of typeEncoding:
(const char *) typeEncoding = 0x000000010bac7c7a "{_NSRange=\"location\"Q\"length\"Q}"
*/
if (idx == NULL) { // 如果为空则表示没有找到'=',此时走远来的流程
[self hc_setNilValueForKey:key];
}
// 处理NSValue的一些场景:比如NSRange、CGRect、CGPoint、CGSize;也就是NSValue structure type
/*
int strncmp(const char *str1, const char *str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字节
如果返回值 < 0,则表示 str1 小于 str2。
如果返回值 > 0,则表示 str2 小于 str1。
如果返回值 = 0,则表示 str1 等于 str2。
*/
NSValue *value;
long cmpLength = idx - typeEncoding;
#define SAME_ENCODE(name) (strncmp(typeEncoding, @encode(name), cmpLength) == 0)
if (SAME_ENCODE(NSRange)) {
value = [NSValue valueWithRange:NSMakeRange(0, 0)];
} else if (SAME_ENCODE(CGPoint)) {
value = [NSValue valueWithCGPoint:CGPointZero];
} else if (SAME_ENCODE(CGSize)) {
value = [NSValue valueWithCGSize:CGSizeZero];
} else if (SAME_ENCODE(CGRect)) {
value = [NSValue valueWithCGRect:CGRectZero];
} else if (SAME_ENCODE(CGVector)) {
value = [NSValue valueWithCGVector:CGVectorMake(0, 0)];
} else if (SAME_ENCODE(UIEdgeInsets)) {
value = [NSValue valueWithUIEdgeInsets:UIEdgeInsetsZero];
} else if (SAME_ENCODE(UIOffset)) {
value = [NSValue valueWithUIOffset:UIOffsetZero];
} else if (SAME_ENCODE(CGAffineTransform)) {
value = [NSValue valueWithCGAffineTransform:CGAffineTransformIdentity];
}
#ifndef FOUNDATION_HAS_DIRECTIONAL_GEOMETRY
else if (@available(iOS 11.0, *)) {
if (SAME_ENCODE(NSDirectionalEdgeInsets)) {
value = [NSValue valueWithDirectionalEdgeInsets:NSDirectionalEdgeInsetsZero];
}
} else {
// Fallback on earlier versions
}
#endif
if (value != nil) {
[self setValue:value forKey:key];
} else {
[self hc_setNilValueForKey:key];
}
}
break;
default:
[self hc_setNilValueForKey:key];
break;
}
}
@end
4. 总结
本文主要是对KVC设置value为nil的一些异常场景的处理,来达到让程序遇到这种setNilValueForKey
的情况不会崩溃;主要的思路就是拿到encoding信息,判断是否是会发生异常的类型,进行处理。
网友评论