對于存在多線程釋放并且并發(fā)訪問的對象,不建議使用weak修飾或訪問。因為weak的底層實現(xiàn)并不完全是線程安全,否則較容易導(dǎo)致over-release而crash。
一、問題
每次版本升級初期總是有少部分會遇到如下的crash

雖然量很少,但總是有也很是煩人。沒辦法只能看下到底是怎么回事。
二、問題描述
很明顯,這是一個over-release的問題;掛在objc_release里;
業(yè)務(wù)代碼如下:
- (void)getIconImage:(NSString*)uri
{
DefineWeakSelfBeforeBlock();
NSString * fullURLString = uri;
if ([fullURLString hasPrefix:@"http://"]) {
fullURLString = [NSString stringWithFormat:@"http:%@", fullURLString];
}
NSURL *url = [fullURLString createURL];
NSString *title=self.title;
[self download:url completion:^(NSData *data, NSError *error) {
DefineStrongSelfInBlock(sself);
UIImage *image=nil;
if(data)
{
// 進(jìn)入自繪邏輯
UIImage * reseponseImage = [UIImage imageWithData:data scale:[UIScreen mainScreen].scale];
if (reseponseImage) {
UIColor * color = [reseponseImage getPresentColor];
image = [UIQuicklinkImageRenderer defaultIconImageWithTitle:title backgroundColor:color];
}
}
if(!image)
{
image = DEFAULT_ICON(title);
}
[sself doCompletion:image error:error];
}];
}
crash堆棧不夠直觀,但是可以簡單猜測出最后訪問sself時肯定出現(xiàn)了over-release了;
但是這是標(biāo)準(zhǔn)的ARC代碼,怎么會出現(xiàn)over-release呢?除非編譯器干了什么,或者objc本身出錯了吧?
三、問題分析
這個問題發(fā)生時有一個可能比較多的場景是,多線程多次并發(fā)回調(diào)該函數(shù)時,因此這個問題很快就能找到原因了;
既然單獨看代碼看不出什么問題,那就直接上匯編找找線索;
根據(jù)猜測crash很可能是sself的over-release問題,那為什么這里會觸發(fā)了release了呢?
objcloadWeakRetained
weak的sself訪問,在這里被ARC轉(zhuǎn)換為了objcloadWeakRetained函數(shù),然后又對retain的該指針,進(jìn)行了objc_release,匯編代碼如下:
如上的代碼實際上訪問sself時變成了如下代碼,即訪問weakSelf被ARC變?yōu)榱薿bjc_loadWeakRetained
0000000101cb7bcc add x0, sp, #0x8 ;獲取sself ; CODE XREF=-[MttQuicklinkCustomIconTask getIconImage:]+688
0000000101cb7bd0 bl imp___stubs__objc_loadWeakRetained
0000000101cb7bd4 mov x21, x0
0000000101cb7bd8 adrp x8, #0x103855000
0000000101cb7bdc ldr x1, [x8, #0xd18]
0000000101cb7be0 mov x2, x23
0000000101cb7be4 mov x3, x20
0000000101cb7be8 bl imp___stubs__objc_msgSend
0000000101cb7bec mov x0, x21
0000000101cb7bf0 bl imp___stubs__objc_release ;release sself
0000000101cb7bf4 mov x0, x23
0000000101cb7bf8 bl imp___stubs__objc_release
0000000101cb7bfc add x0, sp, #0x8
0000000101cb7c00 bl imp___stubs__objc_destroyWeak
那么造成over-release的可能性有2種,一種是objc_release的釋放過程有問題,一種是objc_loadWeakRetained的retain函數(shù)有問題;
關(guān)鍵點出現(xiàn)在objc_loadWeakRetained這個方法上.
其實現(xiàn)如下https://opensource.apple.com/source/objc4/objc4-706/runtime/NSObject.mm.auto.html
id
objc_loadWeakRetained(id *location)
{
id result;
SideTable *table;
retry:
result = *location;
if (!result) return nil;
table = &SideTables()[result];
table->lock();
if (*location != result) {
table->unlock();
goto retry;
}
result = weak_read_no_lock(&table->weak_table, location);
table->unlock();
return result;
}
在lock()之前會先給result取值,那么當(dāng)多線程并發(fā)時,此時某個線程里釋放了location,那么result還是一開始指向location,但接下來走lock,再走weak_read_no_lock,就會無效,因為location的指向已經(jīng)被置為nil了,那就相當(dāng)于retain沒有走,然后返回了被釋放的對象的地址;野指針就產(chǎn)生了;
再接下來使用這個地址干任何事情都是可能crash的;
當(dāng)然蘋果自己在objc注釋里也寫了類似如下注釋
This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
因此,問題的根本很可能是objc_loadWeakRetained的非多線程安全導(dǎo)致的,再結(jié)合發(fā)現(xiàn)用戶遇到的情況基本都是多線程頻繁回調(diào)該函數(shù)的case,那基本可以斷定就是weak的這個特定的鍋了;
解決辦法也很簡單,不適用weak修飾訪問了唄;多retain一會兒也無妨;
四、結(jié)論
所以:對于容易存在多線程釋放并且存在多線程并發(fā)訪問的對象,不建議使用weak修飾或訪問。畢竟weak的底層實現(xiàn)蘋果也說明了,并不完全是線程安全,盡量減少這種情況即可;
這也可以解釋很多系統(tǒng)庫的莫名其妙的over-release問題。