调用获取dispatch_queue_t属性的getter导致崩溃

3
我有一个私有串行队列,它被声明为属性,但我遇到了一个非常奇怪的问题。如果我将该属性作为dispatch_async参数调用,程序会崩溃(EXC_BAD_ACCESS (code=EXC_i386_GPFLT))。在调试后,我发现发生崩溃是由于getter方法被调用。如果不调用getter方法,则不会发生崩溃。此外,每次调用self.queue的第二个时间都会崩溃。看下面的第二个示例。就好像第一次合成的getter调用已经导致ivar被过度释放一样。这是针对iOS 9及以上版本的,因此我没有检查OS_OBJECT_USE_OBJC。 示例1)以下示例无效:
@interface Test ()
@property (nonatomic, strong) dispatch_queue_t initQueue;
@end

- (instancetype)init {
    self = [super init];
    if (self) {
        _initQueue = dispatch_queue_create("com.test.initQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)onCompletion:(void (^)())completion {
  // Crashes here - EXC_BAD_ACCESS (code=EXC_i386_GPFLT) 
  // the second time self.queue is accessed - either by subsequent call into 
  // this method, or by adding NSLog(@"%@", self.queue) before this line.
  dispatch_async(self.initQueue, ^{
    ...
  });
}

例子2)这样也不行:

@interface Test ()
@property (nonatomic, strong) dispatch_queue_t initQueue;
@end

- (instancetype)init {
    self = [super init];
    if (self) {
        _initQueue = dispatch_queue_create("com.test.initQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)onCompletion:(void (^)())completion {
  NSLog(@"%@", self.initQueue);
  // Crashes below - EXC_BAD_INSTRUCTION (code=EXC_i386_INVOP, subcode=0x0) 
  NSLog(@"%@", self.initQueue);
}

示例3)如果我避免使用getter,它将正常工作:

@interface Test ()
@property (nonatomic, strong) dispatch_queue_t initQueue;
@end

- (instancetype)init {
        self = [super init];
        if (self) {
            _initQueue = dispatch_queue_create("com.test.initQueue", DISPATCH_QUEUE_SERIAL);
        }
        return self;
    }

- (void)onCompletion:(void (^)())completion {
  // Works fine
  dispatch_async(_initQueue, ^{
    ...
  });
}

例4)如果我提供getter,它也可以工作:

@interface Test ()
@property (nonatomic, strong) dispatch_queue_t initQueue;
@end

- (instancetype)init {
        self = [super init];
        if (self) {
            _initQueue = dispatch_queue_create("com.test.initQueue", DISPATCH_QUEUE_SERIAL);
        }
        return self;
    }

- (dispatch_queue_t)initQueue {
  return _initQueue;
}

- (void)onCompletion:(void (^)())completion {
  // Works fine
  dispatch_async(self.initQueue, ^{
    ...
  });
}

例子5)如果我使用ivar代替property或者将self.initQueue分配给主队列也可以正常工作。
这种行为的原因是什么?
其他开源库使用dispatch_queue_t的property和getter,但它们没有任何问题。例如:https://github.com/rs/SDWebImage/blob/7e0964f8d90dcd80d535c52dd9f6d5fa7432052b/SDWebImage/SDImageCache.m#L57

1
@Rob 我找到了问题所在 - 是因为属性被称为initQueue。上面的代码都将正常工作,因为我已经将示例重命名为使用queue。看起来像是一个命名空间冲突问题。 - Boon
1
@Boon 我怀疑这与方法族有关,任何以newinit为前缀的方法都被假定为NS_RETURNS_RETAINED,因此返回值不会增加其保留值,所以你的保留计数是错误的。如果它是一个非属性iVar,你可能可以使用_initQueue - Mgetz
1
我建议问题不是命名空间冲突,而是ARC根据方法名称作出推断应该执行什么。我曾看到类似的问题,比如那些以new开头的属性。 - Rob
2
为了方便未来的读者理解上面的评论,当问题首次提出时,属性的名称是“queue”,而不是“initQueue”,但该名称是问题的根源。名称以“init”、“new”、“copy”或“mutableCopy”开头的属性将遇到类似于上面所示的问题。 - Rob
1
我浏览了一下libDispatch源代码,没有看到任何明显的问题,但归根结底,如果尝试释放/释放主队列,则不会出现noop或类似情况,我也不会感到惊讶。例如,如果我编写手动释放代码,错误地但故意地过度释放我创建的队列,它会像预期的那样崩溃,但是如果我尝试过度释放主队列(或全局队列),则不会崩溃。归根结底,如果使用主队列或全局队列进行此操作而未崩溃,我会犹豫从中得出太多结论。 - Rob
显示剩余12条评论
1个回答

3
根据您的评论,您最初将属性命名为initQueue,这反过来创建了一个名为initQueue的方法,违反了ARC方法家族规则。这些规则表明,ARC将自动注释以newinit开头的任何方法作为NS_RETURNS_RETAINED

init系列中的方法隐式消耗其self参数并返回保留的对象。这些属性都不能通过属性更改。

这反过来意味着调用方应该安全地假定他们拥有返回的值的所有权,并且不需要增加保留值。因此,当您尝试使用属性时,ARC没有增加预期的引用计数,但ARC仍然在方法结尾处留下了一个释放调用。这导致您的属性值在类被dealloc之前被释放。
在某些情况下,可以使用属性覆盖此行为。但是,我建议您仅需意识到方法家族,因为它们可能对您的应用程序产生良好的性能影响,特别是对于工厂方法。
其他需要注意的陷阱:

alloccopymutableCopynew系列中的方法——即除init之外的所有当前定义系列中的方法——隐式返回保留的对象,就像它们带有ns_returns_retained属性一样。可以通过将方法注释为ns_returns_autoreleasedns_returns_not_retained来覆盖此行为。

关于这个问题的另一个侧面说明:

程序引起在同一对象上调用两次或更多次init方法是未定义的行为,除了每个 init方法调用可能执行最多一个委托init调用。

遗憾的是编译器似乎并没有警告这个问题。

好奇,如果self.initQueue被分配为dispatch_get_main_queue,为什么这会起作用?也许命名不是唯一的问题。 - Boon
1
@Boon 如果我猜的话,可能是因为主队列不能被释放,甚至可能有一个显式实现的 release 来防止它。 - Mgetz
@Boon - 顺便说一句,如果你不相信这个名称是问题的根源,我建议你查看汇编代码,你会看到不同变量生成的内存处理方式是不同的证据。诚然,这有点难以理解,但你可以清晰地看到 ARC 生成的代码是不同的。 - Rob
@Rob 我完全相信这一点,因为使用 init- 与合成的 getter/setter 导致了问题是完全有道理的。关于 dispatch_get_main_queue() 的情况很好奇。你的解释和 Mgetz 的都很有道理。真希望我能给你们两个都选择最佳答案。 - Boon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接