由于测试用例不可能覆盖到所有情况,因此,上线后面对复杂的用户环境和用户操作,难免出现一些crash问题。crash也是很影响用户体验的,常见的crash异常信息有:
- EXEC_BAD_ACCESS (SIGSEGV/SIGBUS),发生在程序试图房屋无效的内存或试图以内存的保护级别所不允许的方式去访问内存的时候;
- EXEC_CRASH (SIGABRT),异常退出,比如:如果程序初始化时间过长而触发了watch dog,被强制终止,会产生这类异常;
- EXC_BAD_INSTRUCTION (SIGILL),程序执行非法指令时,或者堆栈溢出时会报非法指令异常;
- EXC_RESOURCE,程序消耗的资源过多超过了限制,这是一个从操作系统通知,进程是使用太多的资源。这虽然不是崩溃但也会生成崩溃日志。
其他的异常信息有:
- 0x8badf00d,读起来像:ate bad food,该编码表示应用是因为发生watchdog超时而被iOS终止的。 通常是应用花费太多时间而无法启动、终止或响应用系统事件。
- 0xbad22222,VoIP 应用因为过于频繁重启而被终止。
- 0xdead10cc,读起来像:dead lock,应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止。
- 0xdeadfa11,读起来像:dead fall,应用被用户强制退出。
crash log
App上线后,遇到这样一个crash,测试时未发现。先看下crash日志,略去无用信息。
1 | Incident Identifier: 86D40C68-5158-436E-A619-AB19A8E7006C |
先看下崩溃类型是EXC_BAD_ACCESS,第一反应可能是对象被提前释放了导致读到了错误的内存。
看下崩溃线程的堆栈信息,crash发生在main thread的objc_retain函数的第16条指令。
使用disassemble
命令将objc_retain转换为汇编代码:
1 | libobjc.A.dylib`objc_retain: |
取前后几条相关的指令:
1 | 0x1b53cd428 <+8>: 0xf9400008 ldr x8, [x0] |
熟悉objective-c消息发送objc_msgSend
函数的同学应该对这三条指令不陌生,有关objc_msgSend
函数的分析,有兴趣的同学可以看我的这篇文章:objc_msgSend分析
回到objc_retain
函数中,这三条指令的作用是:
- 当前x0中存的是消息receiver的对象的地址,加载其第一个64bit(即
isa
指针)到x8寄存器中 - 通过
isa
指针与0xffffffff8
得到receiver所属的Class对象obj_class
,并将其保存到x8寄存器中 - 从Class对象首地址+32处(
[x8, #0x20]
)加载数据到x8寄存器的低32位。
看下crash log中有关x8寄存器的内容:
1 | x8: 0x0000000000000000 |
因此,第三条指令ldrb
读到了0x20
内存地址的内容,但是0x20
地址是操作系统保留地址,因此crash code为:
1 | Exception Codes: KERN_EXCEPTION_PROTECTED at 0x0000000000000020 |
原因
通过分析crash log系统收集的其他相关crash日志发现,该bug导致的crash集中发生在同一个类中的[UIView setBackgroundColor]
、initWithFrame:
等处。同样也会导致objc_msgSend + 16
的EXEC_BAD_ACCESS
类型的crash。
我们知道,iOS的隐式动画会统一由CATransaction收集并在当前线程的下一个runloop中统一提交。但是如果下一个runloop中执行动画时,与之关联的UIView/CALayer
已经释放了,就会发生野指针错误。
但是查看相关代码,并没有发现UIView/CALayer
被提前释放的相关代码,那是什么原因呢?
根据苹果的文档,UIKit相关操作必须在主线程中进行,否则可能导致程序异常,那么会不会是子线程刷新UI造成的呢?
查看源码发现一句调用层级比较深的代码:
1 | dispatch_async(dispatch_queue_create("HomeDataRequest", NULL), ^{ |
这里在后台线程中进行了reload tableView的操作。改到主线程中刷新就好了。