常见crash分析——数组越界

1,991 阅读4分钟

程序一般都有BUG,Crash是一种比较严重的BUG。一款优秀的应用程序,要保证没有易现的Crash,并且要保证开发者对Crash的可控性,即可以方便的记录、分析、处理。

@interface ViewController (){
    NSArray *dataArray;
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    UIButton *button = [[UIButton alloc] init];
    button.frame = CGRectMake(100, 100, 200, 100);
    button.backgroundColor = [UIColor yellowColor];
    [button setTitle:@"获取第5个元素" forState:UIControlStateNormal];
    [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    dataArray = @[@"第0个元素",@"第1个元素",@"第2个元素",@"第3个元素"];
}
- (void)buttonClicked:(UIButton *)sender {
    NSLog(@"%@",dataArray[4]);
}
@end

我们在这里声明了一个button和一个元素个数为4的字符串类型的数组,并且给button添加了点击事件,当我们按下button时,就会打印数组中第5个元素。很明显,根本获取不到。于是程序崩溃,并且我们能够看到xcode为我们打印出崩溃的原因。

由于崩溃日志太长了,这次我们就看一些有用的信息

**2021-11-30 09:59:43.637768+0800 ArrayOutOfBrounds[6916:110447] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSConstantArray objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 3]'

***** First throw call stack:
(
0   CoreFoundation                      0x00007fff203fbbb4 __exceptionPreprocess + 242
1   libobjc.A.dylib                     0x00007fff2019ebe7 objc_exception_throw + 48
2   CoreFoundation                      0x00007fff2047bf38 _CFThrowFormattedException + 194
3   CoreFoundation                      0x00007fff2033575b +[NSConstantArray new] + 0
4   ArrayOutOfBrounds                   0x000000010144028d -[ViewController buttonClicked:] + 77
5   UIKitCore                           0x00007fff25002189 -[UIApplication sendAction:to:from:forEvent:] + 83
6   UIKitCore                           0x00007fff2489b573 -[UIControl sendAction:to:forEvent:] + 110
7   UIKitCore                           0x00007fff2489b955 -[UIControl _sendActionsForEvents:withEvent:] + 332
8   UIKitCore                           0x00007fff24897e8c -[UIButton _sendActionsForEvents:withEvent:] + 148
9   UIKitCore                           0x00007fff2489a206 -[UIControl touchesEnded:withEvent:] + 488
10  UIKitCore                           0x00007fff2504295d -[UIWindow _sendTouchesForEvent:] + 1287
11  UIKitCore                           0x00007fff250449df -[UIWindow sendEvent:] + 5295
12  UIKitCore                           0x00007fff2501b4e8 -[UIApplication sendEvent:] + 825
13  UIKitCore                           0x00007fff250b128a __dispatchPreprocessedEventFromEventQueue + 8695
14  UIKitCore                           0x00007fff250b3a10 __processEventQueue + 8579
15  UIKitCore                           0x00007fff250aa1b6 __eventFetcherSourceCallback + 240
16  CoreFoundation                      0x00007fff20369e25 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
17  CoreFoundation                      0x00007fff20369d1d __CFRunLoopDoSource0 + 180
18  CoreFoundation                      0x00007fff203691f2 __CFRunLoopDoSources0 + 242
19  CoreFoundation                      0x00007fff20363951 __CFRunLoopRun + 875
20  CoreFoundation                      0x00007fff20363103 CFRunLoopRunSpecific + 567
21  GraphicsServices                    0x00007fff2c851cd3 GSEventRunModal + 139
22  UIKitCore                           0x00007fff24ffbe63 -[UIApplication _run] + 928
23  UIKitCore                           0x00007fff25000a53 UIApplicationMain + 101
24  ArrayOutOfBrounds                   0x000000010144055e main + 110
25  dyld                                0x000000010164ce1e start_sim + 10
26  ???                                 0x000000010ef624fe 0x0 + 4545979646
27  ???                                 0x0000000000000000 0x0 + 0**
28  ArrayOutOfBrounds                   0x000000010143f000 __dso_handle + 0
)

我们可以看到crash发生时的堆栈信息,在堆栈信息的第二行 objc_exception_throw 代表着抛出异常,通过对堆栈信息的分析我们可以发现 objectAtIndexedSubscript: 是导致exception抛出的关键。

[NSConstantArray objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 3]

我们可以创建一个NSArray的分组,通过methodswizzle将objectAtIndexedSubscript:替换成我们自定义的方法,在自定义的方法中实现对数组越界情况的处理就可以避免crash的产生。

image.png

image.png

- (id)chappie_objectAtIndexedSubscript:(NSInteger)idx {
    if (idx < self.count) {
        return [self chappie_objectAtIndexedSubscript:idx];
    }
    NSLog(@"越界了idx = %lu >= array.count = %lu",idx,self.count);
    return nil;
}

我们在这里自定义了一个objectAtIndexedSubscript:方法,将idx和数组元素个数进行比较,如果越界就返回nil,并打印idx与count信息

我们在load中进行一个methodswizzle的操作,

Method originalMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndexedSubscript:));
Method swizzleMethod = class_getInstanceMethod(self, @selector(chappie_objectAtIndexedSubscript:));
method_exchangeImplementations(originalMethod, swizzleMethod);

注意Method originalMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndexedSubscript:));这句话,因为堆栈信息中消息的接受者是NSConstantArray,所以这里不能填self,那为什么使用objc_getClass去获取类cls而不是直接使用NSConstantArray呢,这里就涉及到类簇的内容了,这里就不过多赘述相关知识。

理论上现在就解决了数组越界的问题,但是为了不必要的麻烦,我们给他添加一个单例,确保只交换一次。

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndexedSubscript:));
        Method swizzleMethod = class_getInstanceMethod(self, @selector(chappie_objectAtIndexedSubscript:));
        method_exchangeImplementations(originalMethod, swizzleMethod);
    });
}
- (id)chappie_objectAtIndexedSubscript:(NSInteger)idx {
    if (idx < self.count) {
        return [self chappie_objectAtIndexedSubscript:idx];
    }
    NSLog(@"越界了idx = %lu >= array.count = %lu",idx,self.count);
    return nil;
}
@end

image.png

可以看到,我们再去进行数组越界的操作也不会导致crash