iOS一个容易被忽略的线程安全问题

262 阅读2分钟

iOS开发中有不少人喜欢用懒加载来重写类里面的属性get方法。

例如有一个属性 numbersData:

@property (nonatomic, strong) UIView *customView;

我希望它一开始不分配内存,只有用到它时再开始分配,就可以这么做:

- (UIView *)customView {
    if (_customView == nil) {
        _customView = [UIView new];
        // 做一些 custom view 的初始化配置
    }
    return _customView;
}

这样做的好处是只有当真正用到 customView 时才回去分配内存,不至于一开始就让 app 的内存占用上升到一个很高的程度。但是这种做法最好不要滥用,比如就只限于UI属性。因为在代码写的规范的情况下UI属性只有在主线程被访问(子线程调用UI相关接口会报错)。

假如不是UI属性,是一个数组或其他自定义类呢?例如这样的代码:

@interface SingletonClass : NSObject
@property (nonatomic, strong) NSMutableArray *numbersData;
@end

@implementation SingletonClass

static SingletonClass *gSharedInst;
static dispatch_once_t gOnceToken;

+ (id)sharedInstance {
    dispatch_once(&gOnceToken, ^{
        gSharedInst = [[SingletonClass alloc] init];
    });
    return gSharedInst;
}

- (NSMutableArray *)numbersData {
    if (_numbersData == nil) {
        NSLog(@"data is nil, creating");
        NSMutableArray *array = [NSMutableArray array];
        [array addObject:@"one"];
        [array addObject:@"two"];
        [array addObject:@"three"];
        _numbersData = array;
    }
    return _numbersData;
}

@end

如果在主线程和子线程同事访问numbersData属性,出现什么情况?

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSArray *data2 = [[SingletonClass sharedInstance] numbersData];
    NSLog(@"data 2 pointer : %p", data2);
});

NSArray *data = [[SingletonClass sharedInstance] numbersData];
NSLog(@"data pointer : %p", data);

这段代码执行完后可以发现,这里 data 和 data2 虽然都来自 numbersData 的赋值,指向内存地址却不一样。通过这一测试可以得知,在这种情况下“懒加载”会加载两次,因为 numbersData 属性是 nonatomic 的,当主线程已经给 _numbersData 赋值时子线程对 _numbersData 的判断还是 nil,所以会再创建一次。后续再操作就会引发风险。

所以在使用懒加载来优化的内存占用时,要注意懒加载方法是不是只在主线程调用,最好加上 dispatch_once 来保证只创建1次。