首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >iOS_Crash 四:的捕获和防护

iOS_Crash 四:的捕获和防护

作者头像
mikimo
发布于 2023-10-26 08:06:11
发布于 2023-10-26 08:06:11
99800
代码可运行
举报
文章被收录于专栏:iOS开发~iOS开发~
运行总次数:0
代码可运行

1.Crash 捕获

根据 Crash 的不同来源,分为以下三类:

1.2.NSException

应用层的异常,未被捕获的异常,导致程序向自身发送了 SIGABRT 信号而崩溃,是应用程序自己可控的。对于未被捕获的异常,是可以通过 try-catchNSSetUncaughtExceptionHandler() 机制类捕获的。

常见的 Exception:

  • NSInvalidArgumentException:非法参数异常。加强对参数的检查,避免传入非法参数,特别是标记为 nonull 的参数。
  • NSRangeException:越界异常
  • NSGenericException:遍历的同时对原集合进行修改
  • NSInternalInconsistencyException:不一致异常。如 NSDictionaryNSMutableNSDictionary 使用。
  • NSFileHandleOperationException:文件处理异常。常见的是存储空间不足
  • NSMallocException:内存异常。如内存不足。 系统定义的所有 ExceptionNSExceptionName

捕获 NSExpection:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 记录之前的Crash回调函数(如果有的话)
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;

+ (void)registerUncaughtExceptionHandler {
    // 将别人之前注册的Crash回调取出并备份
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    // 然后再注册自己的
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

// 崩溃时的回调函数
static void UncaughtExceptionHandler(NSException * exception) {
    // 异常的堆栈信息
    NSArray *stackInfo = [exception callStackSymbols];
    // 出现异常的原因
    NSString *reason = [exception reason];
    // 异常名称
    NSString *name = [exception name];
    // 异常错误报告
    NSString *exceptionInfo = [NSString stringWithFormat:@"uncaughtException异常错误报告:\n name:%@\n reason:\n %@\n callStackSymbols:\n %@", name, reason, [stackInfo componentsJoinedByString:@"\n"]];
    // 保存Crash日志到沙盒cache目录
    [SKTool cacheCrashLog:exceptionInfo name:@"CrashLog(UncaughtException)"];
    // 在自己handler处理完后记得把别人的handler注册回去,形成规范的SOP
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
    // 杀掉程序,这样可以防止同时抛出的SIGABRT被Signal异常捕获
    kill(getpid(), SIGKILL);
}

1.2.C++异常

系统捕获到 C++ 异常后会将其转换为 OC 异常抛出,此时的调用堆栈是在异常发生时的队长;但若转换失败则会调用 __cxa_throw 抛出异常,此时的调用队长是处理异常的堆栈,导致原始异常调用堆栈丢失。

捕获 C++ 异常:

  1. 设置异常处理函数:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);

调用 set_terminate(CPPExceptionTerminate) 设置新的全局终止处理函数并保持旧的函数。

  1. 重写 __cxa_throw
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*)) {
    // 获取调用堆栈并存储
    // 再调用原始的 __cxa_throw 函数
}
  1. 异常处理函数 __cxa_throw 往后执行,进入 set_terminate 设置的异常梳理函数。判断如果是 OC 异常则什么也不多,让 OC 异常机制处理;否则获取异常信息。

1.3.Mach异常

内核层的异常。用户态开发者可以通过 Mach API 设置 threadtaskhot 的异常端口来捕获 Mach 异常。

  • tasks:资源所有权单位。每个任务由一个虚拟地址空间、一个端口权限名称控件、一个或多个线程组成。(类似于进程)
  • threads:任务中 CPU 执行的单位
  • ports:安全的单工通信通道,只能通过发生和接收功能进行访问。

Mach 异常相关的 API 有:

  • task_get_exception_ports:获取 task 的异常端口
  • task_set_exception_ports:设置 task 的异常端口
  • mach_port_allocate:创建调用者指定的端口权限类型
  • mach_port_insert_right:将指定的端口插入目标 task

注意:避免在 Xcode 联调时监听,会死锁。


1.4.Unix 信号

又称 BSD 信号,如果开发者没有捕获 Mach 异常,则会被 host 层的方法 ux_exception() 将异常转换为对应的 Unix 信号,并通过方法 threadsignal() 将信号投递到出错线程。可以同 signal(x, SignalHandler) 来捕获 signal

信号表:

  1. SIGHUP:挂起
  2. SIGINT:程序终止信号 interrupt,在用户键入 INTR 字符(通常是 Ctrl-C)是发出,用于通知前台进程组终止进程。
  3. SIGQUIT:程序退出信号 quit,由 QUIT 字符来控制(通常是Ctrl-),程序在收到该信号退出时会生成 core 文件。
  4. SIGILL:执行非法指令
  5. SIGTRAP:由断点指令或陷阱指令
  6. SIGABRT:程序打断信号 abort。
  7. SIGBUS:非法地址
  8. SIGFPE:致命的算术运算错误
  9. SIGKILL:立即结束程序的运行。不能被阻塞、处理和忽略。
  10. SIGUSR1:用户信号1
  11. SIGSEGV:无效内存访问
  12. SIGUSR2:用户信号2
  13. SIGPIPE:管道破裂。进程间的通信,如管道的异常读写。
  14. SIGALRM:alarm 发出的信号
  15. SIGTERM:终止信号,可被阻塞和处理。通常用来要求程序自己正常退出
  16. SIGSTKFLT:栈溢出
  17. SIGCHLD:子进程退出
  18. SIGCONT:进程继续
  19. SIGSTOP:进程停止
  20. SIGTSTP:进程停止
  21. SIGTTIN:进程停止,后台进程从终端读数据时
  22. SIGTTOU:进程停止,后台进程想终端写数据时
  23. SIGURG:I/O有紧急数据达到当前进程
  24. SIGXCPU:进程的CPU时间篇到期
  25. SIGXFSZ:文件大小超出上限
  26. SIGVTALRM:虚拟时钟超时
  27. SIGPROF:profile 时钟超时
  28. SIGWINVH:窗口大小改变
  29. SIGIO:I/O相关
  30. SIGPWR:关机
  31. SIGSYS:非法的系统调用

Tips: 在终端输入 kill -l 查看所有的 signal 信号。

捕获信号:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 一般需要捕获的信号
static const int g_fatalSignals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};
void installSignalHandler() {
    stack_t ss;
    struct sigaction sa;
    struct timespec req, rem;
    long ret;
    // 申请一块内存空间作为可选的信号处理函数栈使用
    ss.ss_flags = 0;
    ss.ss_size = SIGSTKSZ;
    ss.ss_sp = malloc(ss.ss_size);
    // 使用 sigaltstack 函数通知系统可选的信号处理栈帧的存在及其位置
    sigaltstack(&ss, NULL);
    // 指定 SA_ONSTACK 标志通知系统这个信号处理函数应该在可选的栈帧上面执行注册的信号处理函数
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handleSignalException;
    sa.sa_flags = SA_ONSTACK;
    sigaction(SIGABRT, &sa, NULL);
}

void XXXHandleSignalException(int signal) {
    // 打印堆栈
    NSMutableString *crashInfo = [[NSMutableString alloc] init];
    [crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
    [crashInfo appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {
        [crashInfo appendFormat:@"%s\n", strs[I]];
    }
    NSLog(@"%@", crashInfo);
    // 移除其他 Crash 监听, 防止死锁
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
}

2.Crash 防护

2.1.方法未实现

找不到方法的实现:unrecognized selector sent to instance,查找过程详情可见:iOS_Objective-C 消息发送(消息查找 及 消息转发)过程

解决方案:

NSObject 新增分类,实现消息转发的几个方法来规避 Crash

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) { // 已实现不做处理
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ can't responds %@", NSStringFromClass([self class]), NSStringFromSelector(anInvocation.selector));
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) { // 已实现不做处理
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ can't responds %@", NSStringFromClass([self class]), NSStringFromSelector(anInvocation.selector));
}

2.2.KVC 导致 crash

KVC 的搜索模式详情可见:iOS_KVC:Key-Value Coding-2(访问者搜索模式),当最终找不到对应的key时,会导致 crash。

常见场景:

  • 场景1:key 不存在
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
XXXClass * obj = [[XXXClass alloc] init];
[obj setValue:nil forKey:@"xxx"];
// reason: '[<XXXClass 0x2810bfa80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.'

id value = [obj valueForKey:@"xxx"];
// Thread 1: "[<MOPerson 0x600000c76c10> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx."
  • 场景2:key 为 nil
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
XXXClass* obj = [[XXXClass alloc] init];
[obj setValue:@"value" forKey:nil];
// reason: '*** -[XXXClass setValue:forKey:]: attempt to set a value for a nil key'

// 另外:value 为 nil 不会崩溃
[obj setValue:nil forKey:@"name"];

解决方案:覆写系统会抛出异常的实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (id)valueForUndefinedKey:(NSString *)key {
  NSLog(@"Error: valueForUndefinedKey: %@", key);
  return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
  NSLog(@"Error: setValue:%@ forUndefinedKey: %@", value, key);
}

2.3.KVO 导致 crash

场景:

  • 观察者/被观察者 是局部变量
  • 未实现 observeValueForKeyPath:ofObject:changecontext:
  • 移除未注册的观察者(如:重复移除)

Tips: 重复添加观察者,不会crash,但会回调多次

解决方案:

  • addObserverremoveObserver 必须成对出现
  • 使用 Facebook 的 KVOController 实现

2.4.集合类导致 crash

常见场景:

  • 越界
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NSArray *arr = [NSArray array];
id value = [arr objectAtIndex:1];
// Thread 1: "*** -[__NSArray0 objectAtIndex:]: index 1 beyond bounds for empty array"
  • 塞入 nil
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NSMutableArray *arr = [NSMutableArray array];
[arr addObject:nil];
// Thread 1: "*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil"

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:nil forKey:@"xxx"];
// Thread 1: "*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: xxx)"

解决方案:

  • 使用 runtime 在这些修改方法调用前添加判空处理,详情见:Demo

2.5.其他需要注意场景:

  • performSelector: 必须先判断 respondsToSelector:
  • 调用 delegate 的方法前,必须先判断 respondsToSelector:
  • id 类型不能强转,必须先判断 isKindOfClass:
  • 访问 UIKit 时一定要 dispatch 到 main queue
  • 一个实例,不能保证线程访问安全时,记得要加读写锁
  • dispatch_group_leavedispatch_group_enter 必须成对出现
  • 检查属性的修饰方式 (assign/strong/weak/copy)
  • block 调用前必须判空
  • 遍历结合类型对象时不要同时对其进行修改
  • 耗时操作一定 dispatch 到子线程,避免触发 watchDog
  • Debug 模式开启僵尸模式,方便即时发现问题。
  • 使用 XcodeAddress Sanitizer 检测地址访问越界
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
iOS你不知道的事--Crash分析
原文地址:https://www.jianshu.com/p/56f96167a6e9
iOSSir
2019/06/01
1.6K0
iOS Crash不崩溃
用户在使用App的过程中,经常遇到闪退的情况,体验不太好,本文尝试探索引发闪退的原因,以及在遇到crash的情况下,尽可能的保持程序运行,并及时上报错误。
用户2814378
2022/11/07
2.5K0
iOS Crash不崩溃
iOS-底层原理36:内存优化(一) 野指针探测
下面是Mach异常 与 UNIX信号 的转换关系代码,来自 xnu 中的 bsd/uxkern/ux_exception.c
conanma
2021/10/28
2.6K0
iOS 开发:『Crash 防护系统』(一)Unrecognized Selector
APP 的崩溃问题,一直以来都是开发过程中重中之重的问题。日常开发阶段的崩溃,发现后还能够立即处理。但是一旦发布上架的版本出现问题,就需要紧急加班修复 BUG,再更新上架新版本了。在这个过程中, 说不定会因为崩溃而导致关键业务中断、用户存留率下降、品牌口碑变差、生命周期价值下降等,最终导致流失用户,影响到公司的发展。
程序员充电站
2019/08/23
2.3K0
iOS 开发:『Crash 防护系统』(一)Unrecognized Selector
小萝莉说Crash(二): Unrecognized selector xxx 之 ForwardInvocation
2015年不急不忙地到来,小萝莉为大家奉上新年礼包,祝大家新年快乐,希望开发GGMM们新一年的开发工作更加顺利、安心! ^_^ 在上篇的分享中,小萝莉给大家介绍了一个入门必现的应用崩溃问题 —— Unrecognized selector sent to instance xxx,通过分析其出现的主要场景,给大家提出了一些避免出现此类问题的建议。然而,古语有云:“斩草不除根,则必留后患”(感觉好邪恶的样子,嘿嘿嘿)。 今天,小萝莉就要给大家分享规避此类问题的终极利器 —— ForwardInvocation
腾讯Bugly
2018/03/22
2.5K0
小萝莉说Crash(二): Unrecognized selector xxx 之 ForwardInvocation
RunLoop总结:RunLoop的应用场景(五)阻止App崩溃一次
今天要介绍的RunLoop应用场景感觉很酷炫,我们可能不常用到,但是对于做Crash 收集的 SDK可能会用得比较频繁吧。相比关于RunLoop 可以让应用起死回生,大家都听说过,可是怎么实现呢?今天我就来实际试验一下。
Haley_Wong
2018/08/22
1.8K0
RunLoop总结:RunLoop的应用场景(五)阻止App崩溃一次
Android 平台 Native 代码的崩溃捕获机制及实现
一、背景 在Android平台,native crash一直是crash里的大头。native crash具有上下文不全、出错信息模糊、难以捕捉等特点,比java crash更难修复。所以一个合格的异常捕获组件也要能达到以下目的: 支持在crash时进行更多扩展操作,如: 打印logcat和应用日志 上报crash次数 对不同的crash做不同的恢复措施 可以针对业务不断改进和适应 二、现有的方案 其实3个方案在Android平台的实现原理都是基本一致的,综合考虑,可以基于coffeecatch改进。
腾讯Bugly
2018/03/23
6K0
runtime的那些事(一)——runtime基础介绍
一、 什么是runtime? 二、 runtime 版本 三、 与 runtime 的三种交互方式 四、 消息机制的基本原理与执行流程 五、 动态解析与消息转发
我只不过是出来写写代码
2019/04/22
1.8K0
runtime的那些事(一)——runtime基础介绍
深入理解iOS消息转发机制
消息转发流程图 image 向一个对象发送消息时, 首先会在对象类的cache,method list以及父类对象的cache,method list依次查找SEL对应的IMP 如果没有找到,并
程序员不务正业
2018/06/13
1.7K0
iOS底层原理总结 - 探寻Runtime本质(三)
方法调用的本质 本文我们探寻方法调用的本质,首先通过一段代码,将方法调用代码转为c++代码查看方法调用的本质是什么样的。 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m [person test]; // --------- c++底层代码 ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test")); 通过上述源码可以看出c++底层代码
xx_Cc
2018/07/03
5650
在Android Native层实现Try/Catch异常处理机制
在Native层实现异常处理的关键在于信号处理(Signal Handling)和非局部跳转(Non-Local Jumps)。当程序发生错误(如访问非法内存、除以零等)时,操作系统会向进程发送一个信号。我们可以设置一个信号处理函数(Signal Handler),在收到信号时执行特定的代码。
陆业聪
2024/07/23
4940
在Android Native层实现Try/Catch异常处理机制
iOS Crash防护你看这个就够了-下篇
0x1: Previously 上篇 中讲到了Crash处理流程分为四个环节,也分析了Crash防护的方法,本章来讲下其余三个环节:Crash的拦截、Crash的上报、Crash的后续。 0x2: Crash的拦截 所有的未被防护住的Crash最终会走到这一步,在这里我们必须要保证拦截的 全面性、稳定性尽可能多的拦截到所有类型的异常,同时拦截逻辑本身不能产生异常。那么我们需要通过以下几个方面去考虑。 I: Crash类型 和多数操作系统一样,iOS的异常也基本分为 用户层 系统底层 信号 这三个类别,
QQ音乐技术团队
2021/06/15
1.7K0
程序又崩了?一招精准定位段错误!
  在C/C++程序开发过程中,是不是经常会遇到这种场景:时间紧迫匆忙上线,程序突然崩溃。开发同事拿到日志,一看无法定位。临近节点快要交付,各方领导在催。加班加点痛苦排查,找出问题羞愧。   程序崩溃不可怕,无从排查才尴尬。特别是后台程序,“噶”的悄无声息,似乎它未曾存在过。
开源519
2025/05/12
3270
程序又崩了?一招精准定位段错误!
iOS 开发:『Runtime』详解(一)基础知识
我们都知道,将源代码转换为可执行的程序,通常要经过三个步骤:编译、链接、运行。不同的编译语言,在这三个步骤中所进行的操作又有些不同。
程序员充电站
2019/06/13
1.5K0
iOS学习--NSObject详解
官方对于NSObject的解释如下: The root class of most Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects.
mukekeheart
2020/12/25
1.3K0
再谈 iOS App Crash 防护
在移动开发中,App 的闪退率是工程师十分关注且又头疼的事情。去年,网易杭州研究院曾经针对 crash 的防护有提出『大白健康系统--iOS APP 运行时 Crash 自动修复系统』方案,使得 crash 防护这个想法真正被落实,但至今该方案的具体实现并没有被开源。经过一年的时间,圈子里也有一些开发朋友,基于这套方案设计并开源了自己的 “Baymax”,比如『老司机 iOS 周报第七期』中曾提到的 BayMaxProtector。本文将会针对网易 Baymax 这套方案,结合团队内的实践结果,总结其在生产环境中可能遇到的问题及其解决方案,并提出一些自己对这套方案的思考。友情提示,阅读本文前需对网易『大白健康系统--iOS APP 运行时 Crash 自动修复系统』一文有所了解,该文中已有的实现方案,本文不会再花更多笔墨进行赘述。
会写bug的程序员
2020/06/10
2.3K0
再谈 iOS App Crash 防护
iOS_Crash 异常类型
断点异常类型表示跟踪陷阱(trace trap)中断了该进程。跟踪陷阱使附加的调试器有机会在进程执行的特定点中断进程。 在 ARM 处理器上显示为 EXC_BREAKPOINT(SIGTRAP) 在 x86_64 处理器上显示为 EXC_BAD_INSTRUCTION(SIGILL)
mikimo
2023/10/18
2.7K0
RunLoop在iOS开发中的应用
RunLoop在iOS开发中的应用范围并没有像runtime 那样广泛,我们通过CFRuntime的源代码可知runloop跟线程的是密不可分的,一个线程一定会创建一个对应的runloop,只是主线程创建就自动run了,而子线程只会创建不会自动run。苹果线程管理 Thread Management也说了在线程中利用runloop,
羊羽shine
2019/05/29
2.1K0
Runtime消息转发机制
Class_Nonnull isa OBJC_ISA_AVAILABILITY;
星宇大前端
2019/01/15
8340
iOS Crash 防护你看这个就够了 - 下篇
上篇 中讲到了 Crash 处理流程分为四个环节,也分析了 Crash 防护的方法,本章来讲下其余三个环节。
molier
2022/11/03
8550
iOS Crash 防护你看这个就够了 - 下篇
推荐阅读
相关推荐
iOS你不知道的事--Crash分析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档