谈nonatomic非线程安全问题

背景

由于我对SDWebImage这块比较熟悉,并且基于SDWebImage封装了一层对业务更友好的HTSWebImage库。所以一直以来SDWebImage的相关Crash都是我在负责。

由于图片加载频繁使用到多线程,所以SDWebImage的crash总是时不时会带来些惊喜(惊悚)。也有不少常年未解之谜,明明看上去没有任何问题,但是就是发生了EXC_BAD_ACCESS。

近日,与李卓立同学聊到SDWebImage的crash问题。提到了一个SDWebImageDownloaderOperation类中nonatomic属性imageData非线程安全,而导致的crash。

Crash的堆栈分析

一直以来都知道nonatomic是非线程安全的,但是在这之前,从未真正意识到这个问题,也没有将crash往这方面想过。我们先来看看crash的堆栈。

堆栈1:

Crashed: NSOperationQueue 0x174429d60 :: NSOperation 0x17104ce10 (QOS: DEFAULT)
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000cbd0dbec8
Raw Text
0 libobjc.A.dylib
objc_retain + 16
1 LiveStreaming
SDWebImageDownloaderOperation.m line 428
-[SDWebImageDownloaderOperation URLSession:task:didCompleteWithError:]
2 LiveStreaming
SDWebImageDownloader.m line 311
-[SDWebImageDownloader URLSession:task:didCompleteWithError:]

代码:

427 if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
428 if (self.imageData) {
429 UIImage *image = [UIImage sd_imageWithData:self.imageData];
430 NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
431 image = [self scaledImageForKey:key image:image];

堆栈2:

Crashed: NSOperationQueue 0x1702386a0 :: NSOperation 0x170a57cd0 (QOS: DEFAULT)
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x00000000ac18beb8
Raw Text
0 libobjc.A.dylib
objc_msgSend + 16
1 LiveStreaming
SDWebImageDownloaderOperation.m line 302
-[SDWebImageDownloaderOperation URLSession:dataTask:didReceiveData:]
2 LiveStreaming
SDWebImageDownloader.m line 291
-[SDWebImageDownloader URLSession:dataTask:didReceiveData:]

代码:

301 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
302 [self.imageData appendData:data];
303
304 if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
305

一眼看去,什么鬼。堆栈1,crash在428行,if (self.imageData) 怎么也能crash,ARC下自动管理内存,self.imageData用法没毛病呀,怎么又crash在objc_retain了。再来看看堆栈2,这又是怎么回事,[self.imageData appendData:data];出现了EXC_BAD_ACCESS。又是self.imageData出了毛病。

可是为什么self.imageData会出毛病呢?我们来看看imageData属性定义的方式。

@property (strong, nonatomic, nullable) NSMutableData *imageData;

nonatomic非原子性,也就意味着非线程安全,一直以来大家都是习惯使用nonatomic。这样使用保证了效率,但是却会牺牲掉安全性。在这里就是因为nonatomic非线程安全,多线程同时调用到属性的setter和getter方法就有可能发生crash。

目前项目中因为self.imageData属性而导致的crash,每周大约有700例左右,影响面积非常广大,是项目中最严重的几个crash之一。小疏忽引发大问题,而且如果不往nonatomic方面想,这种问题真的非常难查。

探索nonatomic非线程安全的原因

为什么nonatomic是非线程安全的,这样的crash是怎么发生的呢?我们来看看runtime的源码:

//getter:
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);
}
//setter:
void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}

根据源码,我们可以看到,getter是不会对属性进行retain的,假设当getter执行后,切换到另一个线程,执行setter,setter会对oldValue release,导致oldValue释放。再切回执行getter的线程,getter用到的是已经释放的oldValue。就会发生EXC_BAD_ACCESS的crash。

一般情况下,getter执行后,会在外部对getter获取的属性进行retain,也就是调用objc_retain。但是也许就在getter发生之后,objc_retain之前其他线程执行了setter。这时候,就会导致objc_retain产生EXC_BAD_ACCESS,例如堆栈1,这种crash发生的概率不大,但是架不住SDWebImage被频繁使用。特别是feed上,频繁调用SDWebImage,导致多线程切换。

那么atomic会不会发生问题呢?根据源码,在获取到属性时,atomic下getter会立即对value进行retain,即使setter对oldValue release了。由于getter已经进行retain,属性不会立即释放。只有使用完成之后才会释放。所以atomic会可以保证属性的线程安全

总结

一直以来,大家都习惯性使用nonatomic了,平时不会太注意,小疏忽产生大问题。这个crash在SDWebImage上一直有人上报,也遗留了非常长的时间,一直未解决,如果不是特别注意nonatomic,很难想到crash的原因。而且这类crash很难复现。只能通过线上堆栈来看。更加增加了修复crash的难度。so,以后对于处理多线程的问题,要记得不要再用nonatomic了哦~