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

原文 : 与佳期的我的博客(gonghonglou.com)html

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

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

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

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

咱们来仔细打印下看看:app

// 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 类型。ui

NSDictionary 一样是相似的。那咱们的防御方案里则是 Hook 全这些类型,好比 NSArray 的 Category:spa

+ (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 那些就已经足够了。code

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

Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/Container

后记

相关文章
相关标签/搜索