更自然的解决字典数组插入nil而导致crash
最近在优化项目虽说小优化一直在持续,大版本的优化也进行了两个版本了但是bug列表依旧血淋淋的摆在那里。有的看一眼也能找到问题所在但是有的就是想破头也不知道问题在哪里,毕竟整个项目经过了N个人的手代码风格迥异阅读起来也会有不小的困难,因此在这分享一下解决这些个bug之间遇到的问题和一些看似实用的方法。
首先是字典中插入nil
和数组中插入nil
以及数组的越界问题
有人就会说在插入之前和取数组元素之前判断一下不就解决问题了吗?
那么你在字典中插入数据可能就是类似这样的写法:
NSMutableDictionary *mDict = [NSMutableDictionary dictionary];
[mDict setObject:(string ? : @"") forKey:@"key"];
也许大家会有这种想法:何必在这这么判断呢?写一个公共方法或者宏不就比这个简洁吗,事实确实如此,然而代码中过多的使用宏会在预处理阶段消耗大量的时间从而削弱用户体验,也不知道是什么原因导致在swift
中直接就取消了宏这个东西
同样的在数组中添加元素也是类似的写法:
NSMutableArray *marr = [NSMutableArray array];
[marr addObject:(string ? : @"")];
获取数组中的元素可能会是这样子的
if (index < marr.count) {
NSString *elem = marr[index];
} else {
NSLog(@"越界了");
}
上面这样写在实际项目中完全没有问题,而且也是最简单最暴力的写法,然而人类的创造力是无限的,当然了作为程序员的我们更是有着一颗想要通过自己的双手去改变世界的心(然而也只是想想并没有什么卵用),自然而然的就会衍生出各种各样的写法,比如将获取数组元素修改为下面这个样子:
@interface NSArray (NHAdd)
- (id)objectOrNilAtIndex:(NSUInteger) index;
@end
@implementation NSArray (NHAdd)
- (id)objectOrNilAtIndex:(NSUInteger) index {
return index < self.count ? self[index] : nil;
}
@end
看到这里我相信大家都松了一口气,再也不用写上面无聊反复的判断了,只要在获取数组元素的时候调用上面分类的方法一切崩溃问题都会迎刃而解。
然而现实总是残酷的,残酷的现实如下:
- 调用上面分类的方法那么就意味着我们再也不能像这样
marr[2]
来获取数组的元素,只能通过调用objectOrNilAtIndex:2
来获取元素,在某种意义上来说会降低代码的可读性,当然了苹果也是推荐我们使用更为简单直观的方法来实现功能。 - 笔者在一开始就说过如果一个项目是经过了N个人的✋,那么由于当时的大环境影响不可能在每个取数组元素的地方都是这么写的,那么在没有处理过的地方就有可能会crash掉,那么就有人会说把相应的地方替换一下不就可以了吗?世上无难事只怕有心人,替换当然是可以的,但前提是你得给我一年的时间😠
- 就算在分类里添加了对越界的处理然而打败你的不是天真也不是无邪而是习惯,最怕的就是习惯养成自然,在弹指一挥间你就会将获取元素的方法写成
marr[3]
或者[marr objectAtIndex:3]
那么隐患也就不知不觉的与你随行
到了这了就会有人问:难道就没有一个方法来解决这个问题的吗?不 不 不 笔者说过程序员是一个创造力无限的物种那么这个小问题也就如蚂蚁一样任你摆布😜 😝,自然而然的就引出了在Objective-C
中如操盘手一般的Runtime
简单点来说Runtime
就好比你买了个iPhone X除了能知道你有钱以外还能让你装个逼,同样的Runtime
除了能让别人知道你学富五车外你还能在不经意间装个逼,当然了对于笔者这种仅仅略懂皮毛中九牛一毛的初学者来说还是装不起的,毕竟装逼遭雷劈呀
那么接下来就简单介绍一下本文中用到的Runtime
的几个函数:
1.class_getInstanceMethod
得到相应类中的Method方法,该方法适用于要获取的方法是实例方法
/**
得到相应类中的Method方法
@param cls 目标类
@param name 目标类中的方法
@return 得到Method方法
*/
Method class_getInstanceMethod(Class cls, SEL name)
2.class_getClassMethod
得到相应类中的Method方法,该方法适用于要获取的方法是类方法
/**
得到相应类中的Method方法
@param cls 目标类
@param name 目标类中的方法
@return 得到Method方法
*/
Method class_getClassMethod(Class cls, SEL name)
3.class_addMethod
给类添加一个新的方法和该方法的具体实现
/**
给类添加一个新的方法和该方法的具体实现
@param cls 被添加方法的类
@param name 被添加方法的方法名
@param imp 即 implementation ,表示由编译器生成的、指向实现方法的指针。也就是说,这个指针指向的方法就是我们要添加的方法
@param *type 表示我们要添加的方法的返回值和参数
@return 返回值表示添加成功或者失败
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
4.class_replaceMethod
替换类中已有方法的实现,如果该方法不存在添加该方法
/**
替换类中已有方法的实现,如果该方法不存在添加该方法
@param cls 目标类
@param name 替换方法的方法名
@param imp 被替换方法的实现
@param *types 被替换方法的返回值和参数
@return 返回替换方法的实现
*/
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
5.method_exchangeImplementations
替换两个方法的实现
/**
替换两个方法的实现
@param m1 方法1
@param m2 方法2
*/
void method_exchangeImplementations(Method m1, Method m2)
看到这里就证明已经成功了一半了,接下来就先实现在NSDictionary
中插入nil
的处理:
@interface NSMutableDictionary (NullSafe)
@end
@implementation NSMutableDictionary (NullSafe)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&ondeToken, ^{
id obj = [[self alloc] init];
[obj swizzleMethod:@selector(setObject:forKey:) withMethod:@selector(safe_setObject:forKey:)];
});
}
-(void)swizzleMethod:(SEL)originalSelector withMethod:(SEL)newSelector {
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzleMethod = class_getInstanceMethod(class, newSelector);
Bool didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), class_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
//YES - 说明类中不存在这个方法的实现需要将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(class, newSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
}else {
//NO-存在这个方法的实现只要交换即可
class_exchangeImplementation(originalMethod, swizzleMethod);
}
}
-(void)safe_setObject:(id)value forKey:(NSString *)key {
if (value) {
[self safe_setObject:value forKey:key];
}else {
NSLog(@"[NSMutableDictionary setObject: forKey:], Object cannot be nil");
}
@end
ok 到这里就实现了在NSMutableDictionary
中插入nil
的处理,然而有人就会对类方法+(void)load
产生疑问,其实不用担心,有疑问者可以看笔者的另一篇文章,这里是传送门 load方法和initialize方法
接下来说说在NSMutableArray
中插入nil
的处理
此时你也许可能会想数组中添加元素的方法比如说:addObject:
、insertObject:atIndex:
那么也就意味着需要像上面字典处理nil
一样需要替换两个方法,然而细心的你可能会发现当往数组中插入nil
时无论你用addObject:
还是insertObject:atIndex:
在控制台都会输出同样的一串crash的原因:'*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
so不难看出在调用addObject:
时实际上在API内部还是走的insertObject:atIndex
方法,因此只需要如上那样将insertObject:atIndex
替换即可,代码如下:
@implementation NSMutableArray (NullSafe)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&ondeToken, ^{
id obj = [[self alloc] init];
//这个方法的实现就是上面的那个,实际开发中可提取出来统一使用
[obj swizzleMethod:@selector(insertObject:atIndex:) withMethod:@selector(safe_insertObject:atIndex:)];
});
}
-(void)safe_insertObject:(id)value atIndex:(NSUInteger)index {
if (value) {
[self safe_insertObject:value atIndex:index];
}else {
NSLog(@"object can't be nil");
}
}
@end
然而除了在数组中插入nil
外还可能crash的另外一种原因就是数组越界,即出现这类型的报错原因:'*** -[__NSArrayM objectAtIndex:]: index 3 beyond bounds for empty array'
so有了上面的例子还会怕什么,考虑再三为了阅读方便就将上面的东西再写一遍吧,略微有点强迫症,很恐怖
@implementation NSMutableArray (NullSafe)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&ondeToken, ^{
id obj = [[self alloc] init];
//这个方法的实现就是上面的那个,实际开发中可提取出来统一使用
[obj swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(safe_objectAtIndex:)];
});
}
-(id)safe_objectAtIndex:(NSUInteger)index {
if (index < self.count) {
return [self safe_objectAtIndex:index];
}else {
NSLog(@"index is out of array bounds");
return nil;
}
}
@end
到此为止无论你喜欢用marr[3]
还是[marr objectAtIndex:3]
其实都已经无所谓了,因为都不会发生crash了,也许你会发现一个事实那就是在safe_objectAtIndex
这些方法里面当满足条件的时候为什么要调用safe_objectAtIndex
方法自身而不是调用objectAtIndex:
其实很简单因为原来系统的方法已经被替换为你自己的方法当再次调用系统方法时会一直替换最终造成死循环,可是在你调用你自己的方法时该方法的实现已经被替换为系统的实现就可以顺理成章的取到你想要的值。
其实这里还有一个bug就是在调用insertObject:atIndex:
方法时因为只处理了插入元素是否为空而没有对index
是否越界做处理,正在积极研究同时观众老爷有什么好的办法希望指点一二(这么说好像真的有人会看似的☺)。