1. 崩溃
多指在移动设备(如iOS、Android设备)中或不可移动设备(如:Windows、Linux等设备),
在打开或使用应用程序时出现的突然退出中断的情况(类似于Windows的应用程序崩溃)。
多表现为:应用程序画面一闪而过,随即退回到桌面。
崩溃会影响用户体验,造成用户流失,因此,我们要重视崩溃
根据不同场景,崩溃收集方式不同
Xcode编译期间:
测试机获取:
Xcode->Window->Devices and Simulators
或者
设置->隐私->分析与改进
线上崩溃采集:
封装好的三方,直接接入的:友盟、bugly、听云
开源的SDK:KSCrash、plcrashreporter
2. 崩溃产生的原因
划分方式一:
- cpu无法执行的代码
- 被系统强杀
- 语言触发异常
2.1 cpu无法执行的代码
- 无效指令或操作
- 访问无效地址及不具有权限的内存地址
- 除以0等
- 僵尸对象
…
2.2 被系统强杀
- 应用内存消耗过高,即OOM问题
- 主线程长时间无法响应ANR
- 资源异常
- 死锁
- 非法的应用签名
- 后台执行超时
…
2.3 语言触发异常
- OC语言抛出异常
- C++抛出异常
…
划分方式二:
除了上面的划分方式,崩溃还可以按:软件异常、硬件异常划分
软件异常:
软件异常主要来自 kill(),pthread_kill()。iOS 中的 NSException 未捕获,abort 都属于这种情况。
硬件异常:
硬件的信号始于处理器 trap,是和平台相关的。野指针崩溃大部分是硬件异常。
软件异常的流程是:
软件异常 -> Unix信号
硬件异常的流程是:
硬件异常 -> Mach异常 -> Unix信号
Mach 异常:
- EXC_BAD_ACCESS: 不能访问的内存
- EXC_BAD_INSTRUCTION: 非法或未定义的指令或操作数
- EXC_ARITHMETIC: 算术异常(例如除以0)。iOS 默认是不启用的,所以我们一般不会遇到
- EXC_EMULATION: 执行打算用于支持仿真的指令
- EXC_SOFTWARE:软件生成的异常,我们在 Crash 日志中一般不会看到这个类型,苹果的日志里会是 EXC_CRASH
- EXC_BREAKPOINT:跟踪或断点
- EXC_SYSCALL: UNIX 系统调用
- EXC_MACH_SYSCALL: Mach 系统调用
UNIX 信号:
- SIGSEGV,段错误。访问未分配内存、写入没有写权限的内存等。
- SIGBUS,总线错误。比如内存地址对齐、错误的内存类型访问等。
- SIGILL,执行了非法指令,一般是可执行文件出现了错误。
- SIGFPE ,致命的算术运算。比如数值溢出、NaN数值等。
- SIGABRT,调用 abort() 产生,通过 pthread_kill() 发送。
- SIGPIPE,管道破裂。通常在进程间通信产生。比如采用FIFO(管道)通信的两个进程,读管道* 没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。根据苹果相关文档,可以忽略这个信号。
- SIGSYS,系统调用异常。
- SIGKILL,此信号表示系统中止进程。崩溃报告会包含代表中止原因的编码。exit(), kill(9) 等函数调用。iOS 系统杀进程,如 watchDog 杀进程。
- SIGTRAP,断点指令或者其他trap指令产生。
当我们拦截信号处理之后是可以让程序不崩溃而继续运行的,但是不建议这样做,因为程序已经处于异常不可知状态。
看上图里面最终都会转换为 “UNIX信号”, 是不是代表我们只用监听 “UNIX 信号” 就够了呢?为什么还要拦截 Mach 异常呢?
不是所有的 "Mach异常” 类型都映射到了 “UNIX信号”
“UNIX信号” 在崩溃线程回调,如果遇到 Stackoverflow 问题,已经没有条件(栈空间)再执行回调代码了。
捕获到异常信号后,在处理方法 handleSignalException 里通过 backtrace_symbols 方法就能获取到当前的堆栈信息。
堆栈信息可以先保存在本地,下次启动时再上传到崩溃监控服务器就可以了。
Mach是一个XNU的微内核核心
3. 崩溃日志包含信息
基本信息:崩溃发生的日期、iOS 版本等
进程信息:崩溃进程的相关信息,比如崩溃报告唯一标识符、唯一键值、设备标识
异常信息:异常类型、异常编码、异常的线程;
线程回溯:崩溃时的方法调用栈
崩溃日志调用栈内容:
这样直接看崩溃信息的调用栈,是看不懂的
我们需要一个.DSYM文件,dSYM 是保存 函数地址映射信息的中转文件,其中包含文件名、方法名、行号等信息,是和可执行文件的16进制函数地址一一对应的,通过分析崩溃的崩溃文件可以准确知道具体的崩溃信息。
一个ipa包,对应一个.DSYM文件
每次编译生成的 dSYM都会有所差别,通常 dSYM 中会有一个唯一标识,称作 UUID ,用以区分不同的 dSYM 文件。
UUID(Universally Unique Identifier):一个app包对应一个UUID
UDID(Unique Device Identifier):一个手机设备对应一个UDID
通过.DSYM文件的解析,就可以找到对应的文件名和函数名
具体的做法?
方法一:
具体的做法:
- 首先要通过崩溃信息里面,找到UUID,然后通过UUID找到对应正确的.dsym的
- 找到崩溃日志的关键信息,获取:
偏移量、运行时堆栈地址、运行时APP起始地址
相对于起始地址的偏移量为 29796,运行时堆栈地址为 0x102a47464,运行时APP起始地址为 0x102a40000。
- 通过.dsym拿到起始地址(dSYM 文件中保存中符号表 TEXT 段的起始地址),再加上崩溃日志的偏移量,获取运行时堆栈地址在.dsym的真正地址
- 查找.dsym上的运行地址,解析即可
因为 iOS 加载 Mach-O 文件时为了安全使用了 ASLR(Address Space Layout Randomization) 机制,导致二进制 Mach-O 文件每次加载到内存的首地址都会不一样,但是偏移量,加载地址,起始地址的计算规则是一样的;
从上面我们可以得到 0x102a47464 (运行时地址) = 0x102a40000 (起始地址) + 29796(偏移量)这个公式。
因此通过 dSYM 的起始地址和偏移量就可以计算出 0x102a47464 对应在 dSYM 中的地址为 0x100007464 = 0x0000000100000000 + 29296。
方法二:使用命令atos
跟前面那个方法的理论是一致的,只是命令不一样
atos -o 项目名.app.dSYM/Contents/Resources/DWARF/项目名 -l 基地址(运行初始地址) 偏移后的地址
笔者使用的命令参考如下
atos -o Test.app.dSYM/Contents/Resources/DWARF/Test -l 0x1026b4000 0x1026B9904
方法三:使用symbolicatecrash脚本
symbolicate:象征
symbolicatecrash是一个Perl脚本,随Xcode提供,专门用于符号化崩溃日志。你需要找到这个脚本的位置(通常在Xcode的App包内),然后在命令行中使用它,如下:
使用命令find /Applications/Xcode.app -name symbolicatecrash -type f
获取到symbolicatecrash
/path/to/symbolicatecrash /path/to/MyApp.crash /path/to/MyApp.dSYM > Symbolicated.crash
在桌面上新建一个文件夹,把.app文件、.app.dSYM文件、.crash文件、symbolicatecrash工具,一起放到新建的文件夹下:
然后执行命令:
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
./symbolicatecrash JCrashDemo.crash JCrashDemo.app.dSYM > new.crash
即可获取一个新的解析好的crash文件
方法四:使用其他更便捷的脚本
即,将好几步要执行的步骤,简单化
4. 开发中常见崩溃
- unrecognized selector crash
- KVO crash
- NSNotification crash
- NSTimer crash
- Container crash(数组越界,插nil等)
- NSString crash (字符串操作的crash)
- UI not on Main Thread Crash (非主线程刷UI(机制待改善))
- …
unrecognized selector crash
当一个对象找不到对应的方法实现的时候,会报此类错误
在找不到方法时,查找方法将会进入方法forward流程,系统给了三次补救的机会,
所以我们要解决这个问题,在这三次均可以解决这个问题
KVO crash
KVO监听对象属性值的改变
KVO 日常使用造成崩溃的原因通常有以下几个:
- KVO 添加次数和移除次数不匹配:
- 移除了未注册的观察者,导致崩溃。
- 重复移除多次,移除次数多于添加次数,导致崩溃。
- 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
- 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。
- …
NSNotification crash
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
在iOS9以及iOS9以后,可以不做移除通知操作
NSTimer crash
一般是在定时器被target强引用没有被释放,产生内存泄露,或者在定时器触发的时候导致崩溃
解决方案:
1.NSTimer使用Block,对其target不强引用
2.是否找到一个合适的时机,在确定NSTimer已经失效的情况下,让NSTimer自动invalidate
3.使用中间变量
类族(NSArray,NSMutableArray,NSDictonary,NSMutableDictionary)
- 插入空对象
- 数组越界
- 不可变类型插入数据
- …
建议:
- 加三目运算符,防止数据为空
- 数组取值的时候,判断数组个数
- 遇到可变类型,使用strong修饰
非主线程刷UI
UI刷新,必须在主线程操作
在子线程中,需要刷新UI的时候,要切换到主线程
野指针
定义:当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称野指针
若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,则将产生无法预料的后果。因为此时迷途指针所指向的内存现在包含的已经完全是不同的数据。
解决方法:
Xcode开启僵尸对象监听
使用MLeaksFinder或其他三方库,排查内存泄露问题
使用JJException
,减少App闪退
当然,最好做到:开发过程不使用,正式包开启
参考资料:
iOS 野指针处理
iOS野指针定位总结
Chat-GPT有关野指针的解释
问:iOS野指针错误一般由什么引起的?怎么排查与避免?
答:
iOS 开发中的野指针错误通常是指向一个已经被释放的对象的指针仍然被访问时发生的错误。这种错误可能导致应用崩溃或不稳定的行为。野指针错误的主要原因包括但不限于:
- 对象被提前释放:当你尝试访问一个已经被释放的对象时,就会出现野指针错误。这通常是因为对象的引用计数被错误管理,如被提前释放或没有正确地持有对象。
- 内存管理不当:在手动管理内存(如使用MRC, 手动引用计数)的项目中,如果没有正确地平衡retain和release调用,可能会导致对象提前释放,从而引发野指针错误。
- 指针未初始化或重置:使用未初始化的指针或者在释放对象后未将指针设为nil,也可能导致野指针访问。
如何排查野指针错误:
- 使用Xcode的调试工具:利用Xcode的调试工具,如LLDB调试器,可以帮助你定位野指针的来源。
Zombies
工具尤其有用,它可以帮助追踪已释放对象的访问尝试。 - 代码审查:定期进行代码审查可以帮助发现潜在的内存管理错误,特别是对于使用MRC的项目。
- 静态分析工具:Xcode自带的静态分析工具可以帮助检测潜在的内存管理问题,包括可能导致野指针错误的情况。
如何避免野指针错误:
- 使用自动引用计数(ARC):自动引用计数可以大大减少内存管理错误。确保项目启用ARC,让编译器帮助管理对象的生命周期。
- 指针置nil:在释放对象后,将指向该对象的指针置为nil,这样即使尝试访问该指针,也不会导致崩溃。
- 避免使用未初始化的指针:在声明指针变量时,初始化为nil,以避免使用未初始化的指针。
- 遵循内存管理规则:即使在ARC环境下,也需要理解并遵循基本的内存管理规则,如不要持有非自己创建的对象,除非这个对象已经明确地被retain了。
- 使用弱引用:对于不需要拥有对象的引用,使用弱引用可以避免循环引用和提前释放的问题。
通过采取这些措施,你可以显著减少应用中野指针错误的发生,提高应用的稳定性和可靠性。
不得不说,回答的还蛮好的