递归块中的ARC行为

5

我编写了这两个实用函数:

+ (void)dispatch:(void (^)())f afterDelay:(float)delay {
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay*NSEC_PER_SEC)),
                    dispatch_get_main_queue(),
                    f);
  }

+ (void)dispatch:(void (^)())f withInterval:(float)delay {
    void (^_f)() = nil; // <-- A
    _f = ^{
        f();
        [self dispatch:_f afterDelay:delay]; // <-- B
    };
    [self dispatch:_f afterDelay:delay];
}

这个想法是,你可以调用:

[self dispatch:block afterDelay:delay]; - 以便在特定时间后执行块

[self dispatch:block withInterval:delay]; - 以便定期执行块

现在,如果我直接调用 dispatch:withInterval:,程序将在运行时出现错误。因为当程序尝试在B行执行时,_f的值将为nil;而这又会导致_f保持对A_f的引用。

如果我将A改成以下内容,则可以解决这个问题:

__block void (^_f)() = nil;

在这里,我强调对_f的强引用,因此当代码到达B时,_f的值是最终分配给它的值。问题在于我正在遭受保留周期的影响。

最后,我可以将A更改为:

__block void (^_f)() __weak = nil;

这样做应该可以解决两个问题,但是我发现当代码到达B时,_f的值再次变为nil,因为在它被评估时,_f已经被释放了。

我有几个问题:

  • 在最后一种情况下,为什么_f会被释放?如何告诉ARC至少保留块直到下一个调度调用?
  • 编写这些函数的最佳(并且符合ARC标准)方法是什么?

感谢您的时间。


只是一点提示,GCD 中有一个 void (^)(void) 的类型 -- dispatch_block_t - jscs
谢谢,我记下来了。 - Ale Morales
2个回答

4
我如何告诉ARC至少要保留块直到下一次调度调用?
我会说,通过你使用的__block方法。
这样做的问题在于我正在遇到保留循环问题。
我不明白为什么这会是个问题。你想让你的定时器无限期地触发,对吧?这意味着与它相关联的对象也必须永远存在。只要你调度块,它就会被GCD保留,但是有一个额外的引用似乎不会有害。
如果将来的某个时刻,你决定取消定时器,可以通过设置_f = nil来实现。这将打破保留循环。
编写这些函数的最佳(和符合ARC)的方式是什么?
好吧,最好的方法是使用NSTimer。 但我认为学习如何使用GCD是有趣的。幸运的是,苹果在这里提供了一个计时器示例

好的,但是,每次调用_f时,对_f的引用不会增加吗?

让我们来看看__block是如何工作的。系统会在堆上创建一个全局变量,并将指向该内存的引用(例如,一个带有值A的指针)传递给您的块(例如,位于内存值B处)。

因此,在地址A处有一些内存引用地址B处的内存,反之亦然。正如您所看到的,这里每个对象的保留计数为1;当然,GCD也会保留,但是这个保留计数是恒定的,没有理由增加。

您可以从其他地方将_f置空,然后在GCD完成块后,保留计数将变为0。

为什么使用__weak后它会被释放?

正如我们所见,影响地址B的对象ARC计数的有两个因素:GCD和变量_f。如果将_f设为weak,则在对其进行赋值后,您的块仍然没有来自_f的保留计数,并且由于您实际上还没有运行块,因此它没有来自B行的计数。因此它会立即被释放。


注意。 这就是ARC的美妙之处:每次都会得到这种行为,我们可以按照逻辑跟踪所有发生的事情并推断原因。使用垃圾收集器,这个代码块有时会被释放,有时不会,使得调试这个问题变得非常困难。


好的,但是,每次调用 _f 时,_f 的引用不会增加吗?然后当将 _f 设置为 nil 时,它只会被减少一次,从而导致泄漏?此外,为什么使用 __weak 时它会被释放? - Ale Morales
答案文本添加了详细的注释。 - ilya n.
好的,关于我使用__weak的情况,我会假设_f被释放了,因为GCD保留了该块,但使用了弱引用,因此对_f的强引用为0,它被垃圾回收了,我是正确的吗? - Ale Morales
是的,我只是写得慢。 - ilya n.
“……让调试这个问题变得非常困难。” - 的确,我在 JavaScript 上也遇到过这种情况。谢谢! - Ale Morales
1
有时候苹果只是炒作,但有时候你听他们说话,会觉得:哇,这种情况下他们是100%正确的,比如他们因为这个原因在OSX中弃用了垃圾回收。 - ilya n.

1

_f 需要是一个强引用,否则在 ARC 中分配给它的块可能会立即消失,因为没有强引用。

同时,该块需要访问指向自身的指针,正如您发现的那样,这必须使用一个 __block 变量来完成。从块到自身的强引用将导致保留循环,因此这必须是一个弱引用。

因此,您需要两个变量,一个是强引用,一个是弱引用:

+ (void)dispatch:(void (^)())f withInterval:(float)delay {
    __block __weak void (^_f_weak)() = nil; // a weak __block variable for the block to capture
    void (^_f)() = nil; // a strong variable to hold the block itself
    _f_weak = _f = ^{ // both variables will point to the block
        f();
        [self dispatch:_f_weak afterDelay:delay];
    };
    [self dispatch:_f afterDelay:delay];
}

太棒了!这正是我所需要的,也是我找到的唯一解决方案 - 在寻找了一个小时之后!如果没有__block __weak变量,Xcode 7.3.1会警告内存泄漏。 - David H

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