更自然的解决字典数组插入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是否越界做处理,正在积极研究同时观众老爷有什么好的办法希望指点一二(这么说好像真的有人会看似的☺)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容