崩溃捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | // 1.捕获一些异常导致的崩溃 NSSetUncaughtExceptionHandler (&HandleException); // 2.捕获非异常情况,通过signal传递出来的崩溃 signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); signal(SIGSEGV, SignalHandler); signal(SIGFPE, SignalHandler); signal(SIGBUS, SignalHandler); signal(SIGPIPE, SignalHandler); - ( void )handleException:( NSException *)exception { NSString *message = [ NSString stringWithFormat:@ "崩溃原因如下:\n%@\n%@" , [exception reason], [[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]]; NSLog (@ "%@" ,message); UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:@ "程序崩溃了" message:@ "如果你能让程序起死回生,那你的决定是?" preferredStyle:UIAlertControllerStyleAlert]; // __weak typeof(self) weakSelf = self; UIAlertAction *action = [UIAlertAction actionWithTitle:@ "崩就蹦吧" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { //weakSelf->ignore = YES; self ->ignore = YES ; }]; UIAlertAction *action1 = [UIAlertAction actionWithTitle:@ "起死回生" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { }]; [alertVc addAction:action]; [alertVc addAction:action1]; UIViewController *rootVC = [CrashHandler currentFrontViewController]; [rootVC presentViewController:alertVc animated: YES completion: nil ]; CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); while (!ignore) { for ( NSString *mode in (__bridge NSArray *)allModes) { CFRunLoopRunInMode((CFStringRef)mode, 0.001, false ); } } CFRelease(allModes); NSSetUncaughtExceptionHandler ( NULL ); signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGSEGV, SIG_DFL); signal(SIGFPE, SIG_DFL); signal(SIGBUS, SIG_DFL); signal(SIGPIPE, SIG_DFL); if ([[exception name] isEqual:kSignalExceptionName]) { kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]); } else { [exception raise]; } } void HandleException( NSException *exception) { // 获取异常的堆栈信息 NSArray *callStack = [exception callStackSymbols]; /// NSThread.callStackSymbols NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; [userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey]; CrashHandler *crashObject = [CrashHandler sharedInstance]; NSException *customException = [ NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo]; [crashObject performSelectorOnMainThread: @selector (handleException:) withObject:customException waitUntilDone: YES ]; } void SignalHandler( int signal) { // 这种情况的崩溃信息,就另某他法来捕获吧 NSArray *callStack = [CrashHandler backtrace]; NSLog (@ "信号捕获崩溃,堆栈信息:%@" ,callStack); CrashHandler *crashObject = [CrashHandler sharedInstance]; NSException *customException = [ NSException exceptionWithName:kSignalExceptionName reason:[ NSString stringWithFormat: NSLocalizedString (@ "Signal %d was raised." , nil ),signal] userInfo:@{kSignalKey:[ NSNumber numberWithInt:signal]}]; [crashObject performSelectorOnMainThread: @selector (handleException:) withObject:customException waitUntilDone: YES ]; } |
获取各线程调用堆栈
1. 获取所有线程
1 2 3 4 | thread_act_array_t threads; mach_msg_type_number_t thread_count = 0; const task_t this_task = mach_task_self(); kern_return_t kr = task_threads(this_task, &threads, &thread_count); if (kr != KERN_SUCCESS) { return @ "Fail to get information of all threads" ; <br>} |
2. 获取对应线程最顶层函数,以及esp、ebp指针
1 2 3 4 5 6 7 8 9 10 11 12 13 | kern_return_t thread_get_state ( thread_act_t target_act, //目标线程,通过task_threads接口来获取 thread_state_flavor_t flavor, //线程状态类型,如 [ARM/x86]_THREAD_STATE64 thread_state_t old_state, //线程状态信息,可获取线程调用栈寄存器信息 mach_msg_type_number_t *old_stateCnt //线程状态信息成员数目 ); bool fillThreadStateIntoMachineContext(thread_t thread , _STRUCT_MCONTEXT *machineContext) { mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT; kern_return_t kr = thread_get_state( thread , BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count); return (kr == KERN_SUCCESS); } |
3. 根据地址定位镜像
上一步根据栈帧指针得到的是方法的地址,首先需要判断该方法属于哪个镜像文 件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | uint64_t count = _dyld_image_count(); //image数量 const struct mach_header *header = _dyld_get_image_header(index); //image mach-o header const char *name = _dyld_get_image_name(index); //image name <br>uint64_t slide = _dyld_get_image_vmaddr_slide(index);//ALSR偏移地址static uint32_t imageIndexContainingAddress(const uintptr_t address) { const uint32_t imageCount = _dyld_image_count(); <br> const struct mach_header* header = 0; for (uint32_t iImg = 0; iImg < imageCount; iImg++) {<br> /// 遍历images header = _dyld_get_image_header(iImg); // image mach-o header if(header != NULL) { // Look for a segment command with this address within its range. uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg); // ALSR偏移地址 uintptr_t cmdPtr = firstCmdAfterHeader(header); if (cmdPtr == 0) { continue ; } for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {<br> /// 遍历查找mach-0下的load command 类型为SEGMENT, 找到image地址在进程中的内存 const struct load_command* loadCmd = ( struct load_command*)cmdPtr; if (loadCmd->cmd == LC_SEGMENT) { const struct segment_command* segCmd = ( struct segment_command*)cmdPtr; if (addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) { return iImg; } } else if (loadCmd->cmd == LC_SEGMENT_64) { const struct segment_command_64* segCmd = ( struct segment_command_64*)cmdPtr; if (addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) { return iImg; } } cmdPtr += loadCmd->cmdsize; } } } return UINT_MAX; } |
4. 根据地址、镜像文件,定位符号
1 2 3 4 5 6 7 8 9 | //基址 = 偏移量 + _LINKEDIT段虚拟地址 - _LINKEDIT段文件偏移地址 uintptr_t linkeditBase = (uintptr_t)slide + linkeditSegment->vmaddr - linkeditSegment->fileoff; //符号表的地址 = 基址 + 符号表偏移量 const nlist_t *symbolTable = (nlist_t *)(linkeditBase + symtabCmd- >symoff); //字符串表的地址 = 基址 + 字符串表偏移量 char *stringTab = ( char *)(linkeditBase + symtabCmd->stroff); //符号数量 uint32_t symNum = symtabCmd->nsyms;<br><br> // If n_value is 0, the symbol refers to an external object. // This image has been stripped. The name is meaningless, and // almost certainly resolves to "_mh_execute_header" |
const uintptr_t addressWithSlide = address - imageVMAddrSlide;//address为调用栈内存地址
for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
if(symbolTable[iSym].n_value != 0) {
uintptr_t symbolBase = symbolTable[iSym].n_value;//获取符号的 内存地址(函数指针)
uintptr_t currentDistance = addressWithSlide - symbolBase;
if((addressWithSlide >= symbolBase) &&
(currentDistance <= bestDistance))
{
bestMatch = symbolTable + iSym;//最佳匹配符号地址
bestDistance = currentDistance;//调用栈内存地址与当前符号内存地址距离
}
}
}
if(bestMatch != NULL) {
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
if(bestMatch->n_desc == 16) {
info->dli_sname = NULL;
}
else
{
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
if(*info->dli_sname == '_') {
info->dli_sname++; }
}
}
crash和解决方案:
- unrecognized selector sent to instance 方法找不到
- 数组越界,插入空值
[NSDictionary initWithObjects:forKeys:]
使用此方法初始化字典时,objects和keys的数量不一致时- NSMutableDictionary,
setObject:forKey:
或者removeObjectForKey:
时,key为nil setValue:forUndefinedKey:
,使用KVC对对象进行存取值时传入错误的key或者对不可变字典进行赋值- NSUserDefaults 存储时key为nil
- 对字符串操作时,传递的下标超出范围,判断是否存在前缀,后缀子串时,子串为空
- 使用C字符串初始化字符串时,传入null
- 对可变集合或字符串使用copy修饰并进行修改操作
- 在空间未添加到父元素上之前,就使用autoLayout进行布局
- KVO在对象销毁时,没有移除KVO或者多次移除KVO
- 野指针访问
- 死锁
1-9都可以利用Runtime进行拦截,然后进行一些逻辑处理,防止crash
NSInvalidArgumentException
向容器加入nil,引起的崩溃。
SIGSEGV
访问没有被开辟的内存或者已经被释放的内存。
NSRangeException
数组越界,字符串截取越界
SIGABRT
异常 这是一个让程序终止的标识,会在断言、app内部、操作系统用终止方法抛出。
SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。
SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。
SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
SIGCHLD
子进程结束时, 父进程会收到这个信号。
crash一般产生自 iOS 的微内核 Mach,然后在 BSD 层转换成 UNIX SIGABRT 信号,以标准 POSIX 信号的形式提供给用户。
NSSetUncaughtExceptionHandler
函数用于设置异常处理的回调函数,在程序终止前的最后一刻会调用我们设置的回调函数(Handler),进行崩溃日志的记录
大部分第三方 SDK 也是通过这种方式来收集异常的,当我们通过 NSSetUncaughtExceptionHandler
设置异常处理函数时,会覆盖其它 SDK 设置的回调函数,导致它们无法上报统计,反之,也可能出现我们设置的回调函数被其他人覆盖。
我们可以在调用 NSSetUncaughtExceptionHandler
注册异常处理函数之前,先通过 NSGetUncaughtExceptionHandler
拿到已有的异常处理函数并保存下来。然后在我们自己的处理函数执行之后,再调用之前保存的处理函数就可以了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架