这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
1. 容器越界
下面这个代码运行后点击button会造成容器越界的崩溃。
@interface ViewController ()
{
NSArray *dataArr;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
dataArr = @[@"第1个",@"第2个",@"第3个",@"第4个"];
}
- (IBAction)btnAction:(UIButton *)sender {
NSLog(@"%@" ,dataArr[4]);
}
@end
那么这里如何处理容器越界的崩溃呢? 这里需要写一个NSArray的分类,然后还是使用method-Swizzing来进行处理。看到堆栈里面是objectAtIndexedSubscript报错的,所以需要重写objectAtIndexedSubscript方法。
下面这个方法其实是不正确的,这里还需要调用class_addMethod来查看是否有objectAtIndexedSubscript方法了。
@implementation NSArray (LSArray)
+ (void)load {
Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
Method swizzleMethod = class_getInstanceMethod(self, @selector(lsobjectAtIndexedSubscript:));
method_exchangeImplementations(originalMethod, swizzleMethod);
}
- (id)lsobjectAtIndexedSubscript:(NSUInteger)idx{
if(idx < self.count) {
return [self lsobjectAtIndexedSubscript:idx];
}
NSLog(@"越界了%lu >= %lu",idx,self.count);
return nil;
}
@end
这里用didAddMethod判断是不是有了objectAtIndexedSubscript方法,如果有了则替换方法,如果没有就交换imp。
+ (void)load {
Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
Method swizzleMethod = class_getInstanceMethod(self, @selector(lsobjectAtIndexedSubscript:));
bool didAddMethod = class_addMethod(self, @selector(objectAtIndexedSubscript:), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(lsobjectAtIndexedSubscript:),method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzleMethod);
}
}
这里其实还有问题,需要加一个单例来确保只交换一次。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
Method swizzleMethod = class_getInstanceMethod(self, @selector(lsobjectAtIndexedSubscript:));
bool didAddMethod = class_addMethod(self, @selector(objectAtIndexedSubscript:), method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(lsobjectAtIndexedSubscript:),method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzleMethod);
}
});
}
但是这里运行后发现还是崩溃,这是为什么呢?方法的本质是消息,消息包含接受者和消息的主体(SEl和参数),这里的self是NSArray,但是奔溃信息里面的是__NSArrayI,所以需要把originalMethod里面的class换一下
Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
这样就没有数据越界的问题了。当然,NSArray其他的类簇也需要这样的处理,比如可变数组__NSArrayM, 不可变空数组__NSArray0等。
2. NSSetUncaughtExceptionHandler
crash种类很多,那么有没有办法可以捕获所有的crash呢?苹果提供了一个API. NSSetUncaughtExceptionHandler。
写一个ExceptionHandler类,并添加类方法installUncaughtExceptionHandler,然后在这个方法里面调用NSSetUncaughtExceptionHandler。
+ (void)installUncaughtExceptionHandler {
NSSetUncaughtExceptionHandler(&lsExceptionHandlers);
}
NSSetUncaughtExceptionHandler里面的参数是一个c函数。
void lsExceptionHandlers(NSException *exception) {
NSLog(@"%s",__func__);
int32_t exceptionCount = atomic_fetch_add_explicit(&LGUncaughtExceptionCount,1,memory_order_relaxed);
if (exceptionCount > LGUncaughtExceptionMaximum){
return;
}
NSArray *callStack = [ExceptionHandler lsBackTrace];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
[userInfo setObject:exception.name forKey:LGUncaughtExceptionHandlerSignalExceptionName];
[userInfo setObject:exception.reason forKey:LGUncaughtExceptionHandlerSignalExceptionReason];
[userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
[userInfo setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolKey];
[userInfo setObject:@"LSException" forKey:LGUncaughtExceptionHandlerFileKey];
[[[ExceptionHandler alloc] init] performSelectorOnMainThread:@selector(ls_handleException:) withObject:[NSException exceptionWithName:exception.name reason:exception.reason userInfo:userInfo] waitUntilDone:YES];
}
然后在AppDelegate里面的didFinishLaunchingWithOptions调用这个类方法,确保足够早的收集到crash信息。
[ExceptionHandler installUncaughtExceptionHandler];
那么现在如果程序奔溃的话,那么就会到lsExceptionHandlers里面,这里面可以根据获得的exception来进行崩溃的记录,然后上传到服务器。 这里运行后点击Button会有数组越界的崩溃。看到保存的崩溃的地址,然后去到这个地址。
打开日志文件,看到崩溃信息保存了下来,这样就可以把这个文件传到服务器了。
接下来去看崩溃的堆栈。看到这里有个函数_objc_terminate。
在源码中搜索_objc_terminate,发现实现如下:
之前的文章中iOS 底层探索篇 —— dyld加载流程(上)写到_objc_init也调用了exception_init方法,那么exception_init对异常进行了什么处理呢?
看到这里调用了set方法,这里就是说,这个terminate会一直在跑,如果发生了异常,就会调用_objc_terminate回调函数。所以_objc_init就做了异常的回调处理。
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
回到_objc_terminate,看到上面写的如果是objc object 就会调用uncaught_handler,那么就会调用uncaught_handler方法,那么就寻找uncaught_handler,方法进行了赋值。
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
往下看还有对uncaught_handler赋值的地方,但是搜索确没有地方调用它,所以是上层的API。
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
看到NSSetUncaughtExceptionHandler和objc_setUncaughtExceptionHandler名字非常相似,这里就知道NSSetUncaughtExceptionHandler是对objc_setUncaughtExceptionHandler的封装。之前的调用 (*uncaught_handler)((id)e), 也就是调用外面传进来的回调函数,所以lsExceptionHandlers方法里面也有一个NSException。