程序一般都有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的产生。
- (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
可以看到,我们再去进行数组越界的操作也不会导致crash