Crash 防护方案(三):Container (NSArray、NSDictionary、NSNumber etc.)

原文 : 与佳期的个人博客(gonghonglou.com)

数组越界这类的 Crash 是最简单的也是最容易出现,业务开发过程中很可能操作某个 NSArray 类型的对象时忘记判空或者忘记长度判断而造成数组越界崩溃。所以最好是在线上环境接入这类的 Crash 防护。当然,在开发环境下最好不要接入,避免纵容开发者出现这类遗忘判断的错误。

这类崩溃的防护方案无非就是 Hook 可能产生 Crash 的类的相关方法。之前有过一篇文章是讲这类防护的:从 SafeKit 看异常保护及 Method Swizzling 使用分析
SafeKit 并未 Hook 全可能出现 Crash 的类及其方法,尤其是 NSArray 类簇。

关于类簇这里是苹果官网文档:Class Clusters
以及 sunnyxx 在 从NSArray看类簇 文章里的说法:

Class Clusters(类簇)是抽象工厂模式在 iOS 下的一种实现,众多常用类,如 NSString,NSArray,NSDictionary,NSNumber 都运作在这一模式下,它是接口简单性和扩展性的权衡体现,在我们完全不知情的情况下,偷偷隐藏了很多具体的实现类,只暴露出简单的接口。

我们来仔细打印下看看:

// NSArray
NSLog(@"arr alloc:%@", [NSArray alloc].class); // __NSPlaceholderArray
NSLog(@"arr init:%@", [[NSArray alloc] init].class); // __NSArray0

NSLog(@"arr:%@", [@[] class]); // __NSArray0
NSLog(@"arr:%@", [@[@1] class]); // __NSSingleObjectArrayI
NSLog(@"arr:%@", [@[@1, @2] class]); // __NSArrayI
    
// NSMutableArray
NSLog(@"mutA alloc:%@", [NSMutableArray alloc].class); // __NSPlaceholderArray
NSLog(@"mutA init:%@", [[NSMutableArray alloc] init].class); // __NSArrayM

NSLog(@"mutA:%@", [@[].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1, @2].mutableCopy class]); // __NSArrayM

// NSDictionary
NSLog(@"dict alloc:%@", [NSDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"dict init:%@", [[NSDictionary alloc] init].class); // __NSDictionary0

NSLog(@"dict:%@", [@{} class]); // __NSDictionary0
NSLog(@"dict:%@", [@{@1:@1} class]); // __NSSingleEntryDictionaryI
NSLog(@"dict:%@", [@{@1:@1, @2:@2} class]); // __NSDictionaryI

// NSMutableDictionary
NSLog(@"mutD alloc:%@", [NSMutableDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"mutD init:%@", [[NSMutableDictionary alloc] init].class); // __NSDictionaryM

NSLog(@"mutD:%@", [@{}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1, @2:@2}.mutableCopy class]); // __NSDictionaryM

// NSString
NSLog(@"str:%@", [@"" class]); // __NSCFConstantString

// NSNumber
NSLog(@"num:%@", [@1 class]); // __NSCFNumber

以 NSArray 为例,他在 alloc 阶段生成的是 __NSPlaceholderArray 的中间对象,然后在 init 阶段给这个中间对象发消息,由它做工厂,生成真正的对象。其中 NSMutableArray 生成的都是 __NSArrayM 类型,M 代表的就是 Mutable。NSArray 则区分了数组里:包含 0 个对象时生成的是 __NSArray0 类型,包含 1 个对象生成的是 __NSSingleObjectArrayI 类型,包含多个对象时生成的是 __NSArrayI 类型。

NSDictionary 同样是类似的。那我们的防护方案里则是 Hook 全这些类型,比如 NSArray 的 Category:

+ (void)load {
    
    // [NSArray alloc]
    [NSClassFromString(@"__NSPlaceholderArray") jr_swizzleMethod:@selector(initWithObjects:count:) withMethod:@selector(initWithObjects_guard:count:) error:nil];
    // @[]
    [NSClassFromString(@"__NSArray0") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(guard_objectAtIndex:) error:nil];
    [NSClassFromString(@"__NSArray0") jr_swizzleMethod:@selector(arrayByAddingObject:) withMethod:@selector(guard_arrayByAddingObject:) error:nil];
    // @[@1]
    [NSClassFromString(@"__NSSingleObjectArrayI") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(guard_objectAtIndex:) error:nil];
    [NSClassFromString(@"__NSSingleObjectArrayI") jr_swizzleMethod:@selector(arrayByAddingObject:) withMethod:@selector(guard_arrayByAddingObject:) error:nil];
    // @[@1, @2]
    [NSClassFromString(@"__NSArrayI") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(guard_objectAtIndex:) error:nil];
    [NSClassFromString(@"__NSArrayI") jr_swizzleMethod:@selector(arrayByAddingObject:) withMethod:@selector(guard_arrayByAddingObject:) error:nil];
}

- (instancetype)initWithObjects_guard:(id *)objects count:(NSUInteger)cnt {
    NSUInteger newCnt = 0;
    for (NSUInteger i = 0; i < cnt; i++) {
        if (!objects[i]) {
            break;
        }
        newCnt++;
    }
    self = [self initWithObjects_guard:objects count:newCnt];
    return self;
}

- (id)guard_objectAtIndex:(NSUInteger)index {
    if (index >= [self count]) {
        // 收集堆栈,上报 Crash
        return nil;
    }
    return [self guard_objectAtIndex:index];
}

- (NSArray *)guard_arrayByAddingObject:(id)anObject {
    if (!anObject) {
        // 收集堆栈,上报 Crash
        return self;
    }
    return [self guard_arrayByAddingObject:anObject];
}

当然 NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSString、NSMutableString、NSNumber 这些类都提供了跟多的方法,只要细心仔细的将他们全 Hook 掉就好了。当然实际开发中可能常用的就那么几个方法,Hook 那些就已经足够了。

线上接入了这类的防护之后要比前边的文章讲的 Unrecognized Selector Crash 和 EXC_BAD_ACCESS Crash 更容易造成业务逻辑的错乱,毕竟业务逻辑中不可避免的要用到大量的 NSArray、NSDictionary 类,可能在接入这类防护后会操成点击无响应或者页面卡死,有时候这种情况甚至比程序崩溃还让用户崩溃,所以也要看实际开发需要的取舍。在接入防护后尤其要做好堆栈收集,上报 Crash 的工作,及时解决掉问题。

Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/Container

后记

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

推荐阅读更多精彩内容