谈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:]

代码:

堆栈2:

代码:

一眼看去,什么鬼。堆栈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属性定义的方式。

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

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

探索nonatomic非线程安全的原因

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

根据源码,我们可以看到,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了哦~

Last updated