不安全的weak变量

对于存在多线程释放并且并发访问的对象,不建议使用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问题。

猜你喜欢

转载自blog.csdn.net/rhythmkay/article/details/86539617