对于存在多线程释放并且并发访问的对象,不建议使用weak修饰或访问。因为weak的底层实现并不完全是线程安全,否则较容易导致over-release而crash。
一、问题
每次版本升级初期总是有少部分会遇到如下的crash
虽然量很少,但总是有也很是烦人。没办法只能看下到底是怎么回事。
二、问题描述
很明显,这是一个over-release的问题;挂在objc_release里;
业务代码如下:
- (void)getIconImage:(NSString*)uri
{
DefineWeakSelfBeforeBlock();
NSString * fullURLString = uri;
if ([fullURLString hasPrefix:@"//"]) {
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)
{
// 进入自绘逻辑
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时肯定出现了over-release了;
但是这是标准的ARC代码,怎么会出现over-release呢?除非编译器干了什么,或者objc本身出错了吧?
三、问题分析
这个问题发生时有一个可能比较多的场景是,多线程多次并发回调该函数时,因此这个问题很快就能找到原因了;
既然单独看代码看不出什么问题,那就直接上汇编找找线索;
根据猜测crash很可能是sself的over-release问题,那为什么这里会触发了release了呢?
objcloadWeakRetained
weak的sself访问,在这里被ARC转换为了objcloadWeakRetained函数,然后又对retain的该指针,进行了objc_release,汇编代码如下:
如上的代码实际上访问sself时变成了如下代码,即访问weakSelf被ARC变为了objc_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函数有问题;
关键点出现在objc_loadWeakRetained这个方法上.
其实现如下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取值,那么当多线程并发时,此时某个线程里释放了location,那么result还是一开始指向location,但接下来走lock,再走weak_read_no_lock,就会无效,因为location的指向已经被置为nil了,那就相当于retain没有走,然后返回了被释放的对象的地址;野指针就产生了;
再接下来使用这个地址干任何事情都是可能crash的;
当然苹果自己在objc注释里也写了类似如下注释
This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
因此,问题的根本很可能是objc_loadWeakRetained的非多线程安全导致的,再结合发现用户遇到的情况基本都是多线程频繁回调该函数的case,那基本可以断定就是weak的这个特定的锅了;
解决办法也很简单,不适用weak修饰访问了呗;多retain一会儿也无妨;
四、结论
所以:对于容易存在多线程释放并且存在多线程并发访问的对象,不建议使用weak修饰或访问。毕竟weak的底层实现苹果也说明了,并不完全是线程安全,尽量减少这种情况即可;
这也可以解释很多系统库的莫名其妙的over-release问题。