# 谈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了哦\~
