记一次 _objc_retain 崩溃分析

最近收到 iOS 上一个偶现的 SIGSEGV SEGV_ACCERR 崩溃。错误信息显示该崩溃发生在 _objc_retain 方法,让我困惑了很久。经过分析,发现看似内存问题,实则线程问题。

错误日志

一条典型的错误如下:

1
2
3
4
5
6
7
8
9
0 libobjc.A.dylib       _objc_retain + 16
1 AbcDriver_Example -[AbcRecorder startRecordWithType:] (AbcLocationRecorder.m:151)
2 AbcDriver_Example __40-[AbcLocationReporter onCollectTimer:]_block_invoke (AbcLocationReporter.m:0)
3 Foundation ___NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 16
4 Foundation -[NSBlockOperation main] + 100
5 Foundation ___NSOPERATION_IS_INVOKING_MAIN__ + 20
6 Foundation -[NSOperation start] + 784
7 Foundation ___NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__ + 20
8 Foundation ___NSOQSchedule_f + 180

显示错误发生在 AbcLocationRecorder.m 第151行。第151行代码如下:

1
AbcLocationSample *locationSample = [self prepareDataFromAbcLocation:self.latestLocation regeocode:nil];

代码看起来没有任何问题,也很难将其跟 _objc_retain 方法联系起来。

类似案例

处理 iOS 崩溃的经验不多,所以先在网上找了一圈看是否有人遇到类似问题,还果真找到了。

案例一

iOS崩溃分析 - Lightning_S - 博客园 提到了两个崩溃。

一个是 objc_release,错误如下:

1
2
3
0  libobjc.A.dylib                0x1b394f150 objc_release + 16
1 _appstore 0x10184b694 -[YNP_VRHomeCoreViewModel voiceRoomDidChangeSpeakingUser:] + 373 (YNP_VRHomeCoreViewModel.m:373)
2 Aipai_appstore 0x1015a6144 __63-[YNP_VoiceRoomManager makeDelegatesPerformSelector:obj:async:]_block_invoke + 1633 (YNP_VoiceRoomManager.m:1633)

另一个是 objc_retain,错误如下:

文章的结论是:

  • 以上崩溃都是对象引用计数变化时没有正确加锁(线程不安全)导致
  • 编译器在代码中插入 objc_retainAutoreleasedReturnValue,所以错误日志中会看到 _objc_retain

案例二

从一个crash分析到苹果的代码问题 - 简书 提到属性被声明为 nonatomic 时,当对象在一个线程中释放了,另一个线程访问时就可能出问题。

至于 nonatomic 的线程安全问题,原因如下:

nonatomic取到函数地址后,直接返回指针指向的值,如果这时 *slot 正好被释放,那么返回的就是一个错误的值
而atomic会先retain,然后放到自动释放池,这样就能保证返回的对象一定不会被释放

这里直接贴上相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}

// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;

// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();

// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}

问题复现

看过两个类似案例,再来分析自己的问题就有头绪了。出错的第151行代码如下:

1
2
// 主线程中读 self.latestLocation
AbcLocationSample *locationSample = [self prepareDataFromAbcLocation:self.latestLocation regeocode:nil];

我们很自然地把怀疑目标锁定在 self.latestLocation 这里。latestLocation 属性定义如下:

1
2
3
4
@interface AbcLocationRecorder () <AbcLocationManagerDelegate>
// latestLocation 访问修饰符为 nonatomic
@property (nonatomic, strong) AbcLocation *latestLocation;
@end

位置更新时通过如下回调来更新 self.latestLocation

1
2
3
4
5
6
7
8
9
- (void)mapLocationManager:(AbcLocationManager *)manager didUpdateLocations:(NSArray<AbcLocation *> *)locations {
if (locations.count > 0) {
AbcLocation* location = [locations firstObject];
AbcLog_C(@"定位坐标: %f, %f", location.location.coordinate.latitude, location.location.coordinate.longitude);
// 子线程中写 self.latestLocation
self.latestLocation = location;
AbcLOCATION_UNLOCK(self.lock);
}
}

总结一下:

  • latestLocation 访问修饰符为 nonatomic
  • 主线程中读 self.latestLocation
  • 子线程中写 self.latestLocation
  • 读写 self.latestLocation 没有加锁!

前面提到这个 SIGSEGV SEGV_ACCERR 崩溃在我们 App 中是偶现的,Bugly 上有不少上报,但实际中跟进问题时却一次也没复现。怎么办?

我们写个 demo 吧。demo 如下:

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
@interface ViewController ()
@property (nonatomic, strong) AbcLocation *latestLocation;

@end

@implementation ViewController
- (void)testFun2
{
dispatch_queue_t queue1 = dispatch_queue_create("queue1", 0);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", 0);
dispatch_queue_t queue3 = dispatch_queue_create("queue3", 0);

dispatch_async(queue1, ^{
while (true) {
[NSThread sleepForTimeInterval:0.2f];
//@synchronized (self) {
NSLog(@"last location %@", self.latestLocation);
//}
}
});
dispatch_async(queue3, ^{
while (true) {
[NSThread sleepForTimeInterval:0.2f];
//@synchronized (self) {
NSLog(@"last location %@", self.latestLocation);
//}
}
});
dispatch_async(queue2, ^{
while (true) {
//@synchronized (self) {
self.latestLocation = [[AbcLocation alloc] init];
//}
[NSThread sleepForTimeInterval:0.2f];
}
});
}
@end

运行几次很快就会产生崩溃。找到复现方法后,解决问题就很简单了。多线程读写 self.latestLocation 属性中正确地加锁,保证线程安全就能避免崩溃。

更多知识

网上寻找相似案例,是为了快速弄清问题原因。写demo是为了快速找到问题复现方法和解决办法。但我们不能就此止步,因为很多问题还可以更深入。

objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue

编译器在代码中插入 objc_retainAutoreleasedReturnValue,所以错误日志中会看到 _objc_retain

那什么是 objc_autoreleaseReturnValue 呢?

在 MRC 的环境里有以下代码:

1
2
3
4
// MRC
+ (id) array {
return [[NSMutableArray alloc] init];
}

我们知道内存分配的原则是”谁分配,谁释放”。这个原则让上述代码很为难,

  • 不能在 return 之前释放,因为分配出来的对象还没交给调用方法使用
  • 也不可能在 return 之后释放,因为 return 之后作为分配方没法负责释放了(没有对象指针)

所以需要将分配出来的对象注册到自动释放池,也延迟释放

1
2
3
4
// MRC
+ (id) array {
return [[[NSMutableArray alloc] init] autorelease];
}

ARC 为我们自动调用了 autoreleaseretain 两个方法 (自动释放)。考虑到兼容 MRC 时,ARC 自动调用 autoreleaseretain 两个方法会带来不必要的开销,所以 ARC 使用 objc_autoreleaseReturnValue
objc_retainAutoreleasedReturnValue 对多余操作优化,以提升性能优化。


How does objc_retainAutoreleasedReturnValue work? - Matt Galloway 中对 objc_retainAutoreleasedReturnValue 有更多解释。这里挑关键点翻译出来。

objc_retainAutoreleasedReturnValue 背后的思路是这样的:如果一个函数返回的对象是 autoreleased 的,并且接下来的对这个对象执行的操作是 retain,那么这里的 autoreleaseretain 完全是无意义的,不过是在浪费 CPU 时间。如果某些情况下我们可以检测出这种情况,就能节省CPU时间。在App整个运行期间,节省的CPU时间累计下来就是个不小的优化。

Apple’s code提到:

objc_autoreleaseReturnValue() 检查 return 之后调用方的指令。如果调用方是立即调用 objc_retainAutoreleasedReturnValue,则被调用方不会做 autorelease 操作,而是将结果以 thread-local 方式保存。如果调用方并没有调用 objc_retainAutoreleasedReturnValue,则被调用方会做 autorelease 操作。
objc_retainAutoreleasedReturnValue 会检查返回值是否跟 thread-local 变量中保存的值一致。如果一致,则直接返回结果。如果不一致,则会执行一次 autorelease 和 retain。无论哪种方式,调用方都能拿到一个 retained reference

考虑你有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (SomeClass*)createMeAnObject {
SomeClass *obj = [[SomeClass alloc] init];
obj.string = @"Badger";
obj.number = 10;
return obj;
}

- (id)init {
if ((self = [super init])) {
self.myObject = [self createMeAnObject];
}
return self;
}

我们可以重写以上代码,重写后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (SomeClass*)createMeAnObject {
SomeClass *obj = [[SomeClass alloc] init];
obj.string = @"Badger";
obj.number = 10;
return [obj autorelease];
}

- (id)init {
if ((self = [super init])) {
[myObject release];
SomeClass *temp = [self createMeAnObject];
myObject = [temp retain];
}
return self;
}

如果将 createMeAnObject 内联到 init,则代码变成这样:

1
2
3
4
5
6
7
8
9
10
11
- (id)init {
if ((self = [super init])) {
[myObject release];
SomeClass *temp = [[SomeClass alloc] init];
obj.string = @"Badger";
obj.number = 10;
[temp autorelease];
myObject = [temp retain];
}
return self;
}

注意以上代码中 [temp autorelease] 后紧接着一个 [temp retain]。这正是新的 Objective-C 运行时可以优化的一个点。

(译者注:有很多细节的分析,这里略过) 编译一个调用 objc_autoreleaseReturnValue()objc_retainAutoreleasedReturnValue() 的方法时,编译器会添加如下指令作为标记。

1
2
3
4
5
6
7
8
9
Thumb mode:
f7ffef56 blx _objc_msgSend
463f mov r7, r7
f7ffef54 blx _objc_retainAutoreleasedReturnValue

ARM mode:
ebffffa0 bl _objc_msgSend
e1a07007 mov r7, r7
ebffff9e bl _objc_retainAutoreleasedReturnValue

无论哪种模式,编译器均会添加 mov r7, r7这条看似无任何意义的指令(它将 r7 寄存器的值 move 到 r7寄存器)。不过这条指令是有意义的,编译器将它作为标识,用于告知 objc_autoreleaseReturnValue 方法:调用方接下来会调用 objc_retainAutoreleasedReturnValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id objc_autoreleaseReturnValue(id object) {
if (thumb_mode && next_instruction_after_return == 0x463f ||
arm_mode && next_instruction_after_return == 0xe1a07007)
{
set_flag(object);
return object;
} else {
return objc_autorelease(object);
}
}

id objc_retainAutoreleasedReturnValue(id object) {
if (get_flag(object)) {
clear_flag();
return object;
} else {
return objc_retain(object);
}
}

注:以上是伪代码,x86 版本的代码见这里

总结一下:ARC 出现之前,我们不得不在代码中写 autorelease 和 retain。ARC 出现之后,虽然不用再写 autorelease 和 retain,但是遗留代码中的 autorelease 和 retain 会导致很多冗余操作,objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue 正是为了应对这些冗余操作的优化。

问题二

参考