一个objc_retain+16的crash问题分析

由于测试用例不可能覆盖到所有情况,因此,上线后面对复杂的用户环境和用户操作,难免出现一些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
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
Incident Identifier: 86D40C68-5158-436E-A619-AB19A8E7006C
CrashReporter Key: 82b59c10d300ce0f26b67acba78caf0bb632483c
Hardware Model: iPhone10,3
Process: ***** [5374]
Path: /var/containers/Bundle/Application/BBEFF30F-9DBD-41AC-AC2D-76479532E149/GWMovie.app/GWMovie
Identifier: **********
Version: 9.4.1 (9407)
Code Type: ARM-64
Parent Process: ? [1]

Date/Time: 2018-12-15 19:09:04.000 +0800
OS Version: iOS 12.1 (16B92)
Report Version: 104

Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_EXCEPTION_PROTECTED at 0x0000000000000020
Crashed Thread: 0

Thread 0 Crashed:
0 libobjc.A.dylib objc_retain + 16
1 QuartzCore CA::AttrList::set(unsigned int, _CAValueType, void const*) + 464
2 QuartzCore CAAnimation_setter(CAAnimation*, unsigned int, _CAValueType, void const*) + 232
3 QuartzCore -[CAAnimation setDelegate:] + 48
4 UIKitCore -[UIViewAnimationState setAnimationAttributes:correctZeroDuration:skipDelegateAssignment:customCurve:] + 876
5 UIKitCore -[UIViewAnimationState animationForLayer:forKey:forView:] + 1184
6 UIKitCore -[UIViewAnimationState actionForLayer:forKey:forView:] + 120
7 UIKitCore +[UIView(Animation) _defaultUIViewActionForLayer:forKey:] + 112
8 UIKitCore -[UIView(UIKitManual) actionForLayer:forKey:] + 312
9 QuartzCore -[CALayer actionForKey:] + 136
10 QuartzCore CA::Layer::begin_change(CA::Transaction*, unsigned int, objc_object*, objc_object*&) + 200
11 QuartzCore CA::Layer::setter(unsigned int, _CAValueType, void const*) + 312
12 QuartzCore -[CALayer setContentsMultiplyColor:] + 64
13 UIKitCore -[_UILabelLayer setContentsMultiplyColor:] + 60
14 UIKitCore UILabelCommonInit + 204
15 UIKitCore -[UILabel initWithFrame:] + 60
16 GWMovie -[GWHomePosterButtonView loadAllControls] (GWHomePosterButtonView.m:89)
17 GWMovie -[GWHomePosterButtonView initWithFrame:] (GWHomePosterButtonView.m:109)
18 GWMovie -[GWHomeBaseClassView loadAllControls] (GWHomeBaseClassView.m:102)
19 GWMovie -[GWHomeBaseClassView initWithFrame:] (GWHomeBaseClassView.m:153)
20 GWMovie -[GWHomeDramaBaseClassView initWithFrame:] (GWHomeDramaBaseClassView.m:176)
21 GWMovie -[GWHomeDramaTableViewCell dramaCardView] (GWHomeDramaTableViewCell.m:125)
22 GWMovie -[GWHomeViewController tableView:cellForRowAtIndexPath:] (GWHomeViewController.m:377)
23 UIKitCore -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 684
24 UIKitCore -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 80
25 UIKitCore -[UITableView _updateVisibleCellsNow:isRecursive:] + 2256
26 UIKitCore -[UITableView layoutSubviews] + 140
27 UIKitCore -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1380
28 QuartzCore -[CALayer layoutSublayers] + 184
29 QuartzCore CA::Layer::layout_if_needed(CA::Transaction*) + 324
30 QuartzCore CA::Context::commit_transaction(CA::Transaction*) + 340
31 QuartzCore CA::Transaction::commit() + 608
32 QuartzCore CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 92
33 CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
34 CoreFoundation __CFRunLoopDoObservers + 412
35 CoreFoundation __CFRunLoopRun + 1264
36 CoreFoundation CFRunLoopRunSpecific + 436
37 GraphicsServices GSEventRunModal + 100
38 UIKitCore UIApplicationMain + 212
39 GWMovie main (main.m:16)
40 libdyld.dylib start + 4

...
...
...

Thread 0 crashed with ARM-64 Thread State:
cpsr: 0x00000000a0000000 fp: 0x000000016ae03800 lr: 0x00000001bbceb3e8 pc: 0x00000001b6881430
sp: 0x000000016ae037c0 x0: 0x00000001078e22b0 x1: 0x0000000000000002 x10: 0x00000001f127e100
x11: 0x0000000000000354 x12: 0x0000000000000154 x13: 0x0000000000000154 x14: 0x000000000000007f
x15: 0x00000000ffffffe8 x16: 0x00000001b72a39e4 x17: 0x0000010000000100 x18: 0x0000000000000000
x19: 0x0000000107b345b0 x2: 0x0000000000000303 x20: 0x00000001078e22b0 x21: 0x0000000107b34e60
x22: 0x0000000000000002 x23: 0x000000000000008c x24: 0x0000000107b345b8 x25: 0x0000000107b34bd0
x26: 0x0000000282a1cc60 x27: 0x0000000000000000 x28: 0x0000000106ff3d10 x29: 0x000000016ae03800
x3: 0x0000000000000002 x4: 0x0000000000000000 x5: 0x0000000000000000 x6: 0x000000016ae036a0
x7: 0x0000000000000000 x8: 0x0000000000000000 x9: 0x00000001f127a070

先看下崩溃类型是EXC_BAD_ACCESS,第一反应可能是对象被提前释放了导致读到了错误的内存。
看下崩溃线程的堆栈信息,crash发生在main thread的objc_retain函数的第16条指令。

使用disassemble命令将objc_retain转换为汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
libobjc.A.dylib`objc_retain:
0x1b53cd420 <+0>: 0xb40001a0 cbz x0, 0x1b53cd454 ; <+52>
0x1b53cd424 <+4>: 0xb7f80180 tbnz x0, #0x3f, 0x1b53cd454 ; <+52>
0x1b53cd428 <+8>: 0xf9400008 ldr x8, [x0]
0x1b53cd42c <+12>: 0x927d8108 and x8, x8, #0xffffffff8
0x1b53cd430 <+16>: 0x39408108 ldrb w8, [x8, #0x20]
0x1b53cd434 <+20>: 0x36100128 tbz w8, #0x2, 0x1b53cd458 ; <+56>
0x1b53cd438 <+24>: 0xb25303e8 orr x8, xzr, #0x200000000000
0x1b53cd43c <+28>: 0xc85f7c09 ldxr x9, [x0]
0x1b53cd440 <+32>: 0x36000149 tbz w9, #0x0, 0x1b53cd468 ; <+72>
0x1b53cd444 <+36>: 0xab080129 adds x9, x9, x8
0x1b53cd448 <+40>: 0x54000142 b.hs 0x1b53cd470 ; <+80>
0x1b53cd44c <+44>: 0xc80a7c09 stxr w10, x9, [x0]
0x1b53cd450 <+48>: 0x35ffff6a cbnz w10, 0x1b53cd43c ; <+28>
0x1b53cd454 <+52>: 0xd65f03c0 ret
0x1b53cd458 <+56>: 0xd01d0348 adrp x8, 237674
0x1b53cd45c <+60>: 0x913c8108 add x8, x8, #0xf20 ; =0xf20
0x1b53cd460 <+64>: 0xf9400101 ldr x1, [x8]
0x1b53cd464 <+68>: 0x17fffe3f b 0x1b53ccd60 ; objc_msgSend
0x1b53cd468 <+72>: 0xd5033f5f clrex
0x1b53cd46c <+76>: 0x140003ea b 0x1b53ce414 ; objc_object::sidetable_retain()
0x1b53cd470 <+80>: 0xd5033f5f clrex
0x1b53cd474 <+84>: 0x52800001 mov w1, #0x0
0x1b53cd478 <+88>: 0x14000a05 b 0x1b53cfc8c ; objc_object::rootRetain_overflow(bool)

取前后几条相关的指令:

1
2
3
0x1b53cd428 <+8>:  0xf9400008   ldr    x8, [x0]
0x1b53cd42c <+12>: 0x927d8108 and x8, x8, #0xffffffff8
0x1b53cd430 <+16>: 0x39408108 ldrb w8, [x8, #0x20]

熟悉objective-c消息发送objc_msgSend函数的同学应该对这三条指令不陌生,有关objc_msgSend函数的分析,有兴趣的同学可以看我的这篇文章:objc_msgSend分析

回到objc_retain函数中,这三条指令的作用是:

  1. 当前x0中存的是消息receiver的对象的地址,加载其第一个64bit(即isa指针)到x8寄存器中
  2. 通过isa指针与0xffffffff8得到receiver所属的Class对象obj_class,并将其保存到x8寄存器中
  3. 从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 + 16EXEC_BAD_ACCESS类型的crash。

我们知道,iOS的隐式动画会统一由CATransaction收集并在当前线程的下一个runloop中统一提交。但是如果下一个runloop中执行动画时,与之关联的UIView/CALayer已经释放了,就会发生野指针错误。

但是查看相关代码,并没有发现UIView/CALayer被提前释放的相关代码,那是什么原因呢?

根据苹果的文档,UIKit相关操作必须在主线程中进行,否则可能导致程序异常,那么会不会是子线程刷新UI造成的呢?

查看源码发现一句调用层级比较深的代码:

1
2
3
4
5
6
7
dispatch_async(dispatch_queue_create("HomeDataRequest", NULL), ^{
...
...
[self.homeTableView refreshTableView];
...
...
});

这里在后台线程中进行了reload tableView的操作。改到主线程中刷新就好了。