今天遇到群友们在讨论一个处理NSNull的问题;
在JSON格式化时,value是null的会被转换为NSNull对象:
/*
{
"type" : 1,
"time" : null,
"avatar" : ""
}
*/
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"code" ofType:@"json"]];
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
id value1 = dic[@"time"];
id value2 = dic[@"name"];
NSLog(@"\n%@---%@--\n%@",value1,value2,dic);
从输出可以看到time的值转换为NSNull对象,它是<null>
取一个不存在的key,输出,它是(null)
输出结果
(null)就是nil,在OC里对nil发送消息是安全的,[(NSString *)value2 length]什么都不会发生;
如果调用[(NSString *)value1 length],就会出现NSNull length]: unrecognized selector sent to instance;
到这里都是老生常谈了;
这时候有老哥说,能不能全局来处理这个问题,把NSNull转换为安全的nil;
首先想到的方法就是交换方法,简单实用且彻底.
NSDictionary的取值有两个方法,valueForKey:和objectForKeyedSubscript:,后者是NSExtendedDictionary增加的下标方法.也就是dic[@"time"]的写法;
增加一个分类来交换方法,如果遇到NSNull就返回nil.
#import "NSDictionary+Safe.h"
#import <objc/runtime.h>
@implementation NSDictionary (Safe)
+ (void)load{
Method originM1 = class_getInstanceMethod(self, @selector(valueForKey:));
Method swizzledM1 = class_getInstanceMethod(self, @selector(glvalueForKey:));
method_exchangeImplementations(originM1, swizzledM1);
Method originM2 = class_getInstanceMethod(self, @selector(objectForKeyedSubscript:));
Method swizzledM2 = class_getInstanceMethod(self, @selector(globjectForKeyedSubscript:));
method_exchangeImplementations(originM2, swizzledM2);
}
- (id)glvalueForKey:(NSString *)key{
id value = [self glvalueForKey:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
- (id)globjectForKeyedSubscript:(id)key{
id value = [self globjectForKeyedSubscript:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
@end
VC里还是刚才的代码,试一下,竟然不好使,value1还是<null>,[(NSString *)value1 length]还是会crash;
哦,差点忘了重要的事,对于类簇只操作一个公共父类是不行的,这里不能用分类了,得多操作几个.
字典的类是__NSDictionaryM
于是改造一下
+ (void)load{
[self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"__NSDictionaryM"];
[self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscript:) classNmae:@"__NSDictionaryM"];
[self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"__NSSingleEntryDictionaryI"];
[self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(objectForKeyedSubscript:) classNmae:@"__NSSingleEntryDictionaryI"];
[self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"NSDictionary"];
[self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscript:) classNmae:@"NSDictionary"];
}
+ (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
method_exchangeImplementations(originM, swizzledM);
}
尝试输出length
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"code" ofType:@"json"]];
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
id value1 = dic[@"time"];
id value2 = dic[@"name"];
NSLog(@"\n%@---%@--\n%@\nlength=%ld",value1,value2,dic,[(NSString *)value1 length]);
果然可以了
value1也是(null)了
看似清晰了,但是问题还远没有搞定,刚才替换了objectForKeyedSubscript:和valueForKey:还没有试一试valueForKey:行不行,把value换成下面这样,果然还是可以的.
id value1 = [dic valueForKey:@"time"];
那再试试不可变的呢
NSDictionary *dic = @{@"photo" : [NSNull new]};
id s1 = dic[@"photo"];
id s2 = [dic valueForKey:@"photo"];
NSLog(@"\n%ld -- %ld",[(NSString *)s1 length],[(NSString *)s2 length]);
运行,也没问题
不可变的字典
是不是真的没问题了,这时候有老哥说我不需要可变的,只保留了"__NSSingleEntryDictionaryI"和"NSDictionary",结果不改不要紧,一改出大问题;
注释了"__NSDictionaryM"的交换方法,上面那个不可变字典的例子缺crash了,具体来说是valueForKey:获取的s2闪退了,因为s2仍然是NSNull对象,不是nil,转换失败.
接着又试着把"__NSSingleEntryDictionaryI"和"NSDictionary"的交换方法来回注释,总是只保留两个,发现可变和不可变字典可能转换成功,也可能转换失败,及其混乱.
+ (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
method_exchangeImplementations(originM, swizzledM);
}
回头看看这个方法,两个选择器都是className的方法,如果swizzledM都固定成NSDictionary呢;
IMP作为函数指针,假设自定义的方法是P,A和P交换,调用A会走P,然后B又跟P交换,调用A直接变成B了,为了验证我们可以写一个demo试一试.
#import "Father.h"
#import <objc/runtime.h>
@implementation Father
+ (void)load{
[self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodB)];
[self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodC)];
}
- (instancetype)init{
if(self = [super init]){
[self methodA];
[self methodB];
[self methodC];
}
return self;
}
+ (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2{
Method originM = class_getInstanceMethod(self, sel1);
Method swizzledM = class_getInstanceMethod(self, sel2);
method_exchangeImplementations(originM, swizzledM);
}
- (void)methodA{
NSLog(@"A");
}
- (void)methodB{
NSLog(@"B");
}
- (void)methodC{
NSLog(@"C");
}
@end
首先互换对象自己的方法,用这样一个简单的例子就能看到,这里调用方法ABC,输出的是"CAB";
假设A是0x001,B是0x002,C是0x003,第一轮变成0x002,0x001,0x003,第二轮变成0x003,0x001,0x002
输出结果
那如果是交换父类的方法呢,创建出Father的子类SonA,结果也是一样的,代码就不贴了;
如果SonA重写了方法呢,
+ (void)load{
[self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodB) classNmae:@"SonA"];
[self swizzlingWithSel1:@selector(methodB) sel2:@selector(methodC) classNmae:@"SonA"];
}
@implementation SonA
- (void)methodA{
NSLog(@"new A");
}
@end
这段代码sonA重写了methodA,然后自己和自己交换,结果和之前是一样的
结果还是一样的
+ (void)load{
[self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodB) classNmae1:@"SonA" classNmae2:@"SonB"];
[self swizzlingWithSel1:@selector(methodB) sel2:@selector(methodC) classNmae1:@"SonA" classNmae2:@"SonB"];
}
+ (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae1:(NSString *)className1 classNmae2:(NSString *)className2{
Method originM = class_getInstanceMethod(NSClassFromString(className1), sel1);
Method swizzledM = class_getInstanceMethod(NSClassFromString(className2), sel2);
method_exchangeImplementations(originM, swizzledM);
}
A和B交换也是一样的,同样如果B也重写了,那就会和B的方法交换,没重写,那就是和父类方法交换,其实都是一回事,和我们理解的IMP没有出入.
现在回到一开始的问题,到这里就已经清晰了,首先字典替换后的globjectForKeyedSubscript:方法是写在NSDictionary的分类里的,NSDictionary的子类__NSDictionaryM等,都没有实现gl方法,当调用替换的时候,他们都会走父类的这个新增的gl方法,load的最后一行父类的gl方法和原本的方法互换,也就是说,_NSDictionaryM调用valueForKey:其实是走了分类里的glvalueForKey:,最后glvalueForKey调用父类原本的valueForKey:
即使__NSDictionaryM重写了valueForKey:方法,也没能生效,并且__NSDictionaryM的superclass是NSMutableDictionary,NSMutableDictionary的superclass才是NSDictionary
如果考虑到NSArray格式化也会存在NSNull,把NSArray的几个子类实现交换方法,会出现一堆的问题,比如method only defined for abstract class.或者系统内部的越界等等.
所以最后,这算是一个乌龙,因为都忽略了一个重要问题,
多个子类的方法都跟同一个方法交换显然是不行的,应该给每一个子类都准备一个方法
NSDictionary
@implementation NSDictionary (Safe)
+ (void)load{
[self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKeyM:) classNmae:@"__NSDictionaryM"];
[self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscriptM:) classNmae:@"__NSDictionaryM"];
[self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKeyI:) classNmae:@"__NSSingleEntryDictionaryI"];
[self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscriptI:) classNmae:@"__NSSingleEntryDictionaryI"];
[self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"NSDictionary"];
[self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscript:) classNmae:@"NSDictionary"];
}
+ (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
method_exchangeImplementations(originM, swizzledM);
}
- (id)glvalueForKeyI:(NSString *)key{
id value = [self glvalueForKeyI:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
- (id)globjectForKeyedSubscriptI:(id)key{
id value = [self globjectForKeyedSubscriptI:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
- (id)glvalueForKeyM:(NSString *)key{
id value = [self glvalueForKeyM:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
- (id)globjectForKeyedSubscriptM:(id)key{
id value = [self globjectForKeyedSubscriptM:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
- (id)glvalueForKey:(NSString *)key{
id value = [self glvalueForKey:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
- (id)globjectForKeyedSubscript:(id)key{
id value = [self globjectForKeyedSubscript:key];
if([value isKindOfClass:NSNull.class]){
return nil;
}
return value;
}
@end
NSArray
@implementation NSArray (GLSafe)
+ (void)load{
[self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndexSingleI:) classNmae:@"__NSSingleObjectArrayI"];
[self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscriptSingleI:) classNmae:@"__NSSingleObjectArrayI"];
[self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndex0:) classNmae:@"__NSArray0"];
[self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscript0:) classNmae:@"__NSArray0"];
[self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndexI:) classNmae:@"__NSArrayI"];
[self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscriptI:) classNmae:@"__NSArrayI"];
[self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndexM:) classNmae:@"__NSArrayM"];
[self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscriptM:) classNmae:@"__NSArrayM"];
}
+ (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
method_exchangeImplementations(originM, swizzledM);
}
- (id)globjectAtIndexSingleI:(NSUInteger)index{
id obj = [self globjectAtIndexSingleI: index];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndexedSubscriptSingleI:(NSUInteger)idx{
id obj = [self globjectAtIndexedSubscriptSingleI:idx];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndex0:(NSUInteger)index{
id obj = [self globjectAtIndex0: index];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndexedSubscript0:(NSUInteger)idx{
id obj = [self globjectAtIndexedSubscript0:idx];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndexI:(NSUInteger)index{
id obj = [self globjectAtIndexI: index];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndexedSubscriptI:(NSUInteger)idx{
id obj = [self globjectAtIndexedSubscriptI:idx];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndexM:(NSUInteger)index{
id obj = [self globjectAtIndexM: index];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
- (id)globjectAtIndexedSubscriptM:(NSUInteger)idx{
id obj = [self globjectAtIndexedSubscriptM:idx];
if([obj isKindOfClass:NSNull.class]){
return nil;
}
return obj;
}
@end
实际上这种通过runtime来处理Foundation的异常问题并不可靠,如果cocoa内部或者别的库就是会往集合里存NSNull,那这就会出现大问题,类似的情况还有通过runtime来防止数组越界等,越界是阻止了,但同时错误也会被隐藏,抛出异常比隐藏异常更有用,应该去收集crash,然后从触发的原因着手在根本上避免crash.
网友评论