当实现API时,如何避免在块中捕获自身?

221

我有一个可用的应用程序,并且正在将其转换为Xcode 4.2中的ARC。其中一个预检查警告涉及在块中强烈捕获self,导致保留循环。我已经制作了一个简单的代码示例来说明这个问题。我相信我理解了这意味着什么,但不确定实现这种情况的“正确”或推荐方式。

  • self是类MyAPI的一个实例
  • 下面的代码简化了与我的问题相关的对象和块之间的交互
  • 假设MyAPI从远程源获取数据,而MyDataProcessor 对该数据进行处理并生成输出
  • 处理器使用块来通信进度和状态

代码示例:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

问题:我做错了什么,和/或者我应该如何修改以符合ARC规范?

8个回答

508

简短回答

不要直接访问self,而应该通过一个不会被保留的引用间接访问它。如果你没有使用自动引用计数(ARC),可以这样做:

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}
__block 关键字标记了变量,可以在块内部进行修改(我们不这样做),但是当块被保留时它们也不会自动保留(除非您使用ARC)。如果这样做,您必须确信,在释放 MyDataProcessor 实例后,没有其他内容会尝试执行该块。(鉴于您的代码结构,那不应该成为问题。)阅读更多关于 __block 的信息如果您正在使用 ARC,则 __block 的语义会发生变化,并且引用将会被保留,在这种情况下,您应该将其声明为 __weak
长答案
假设您有以下代码:
self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

这里的问题在于self保留了对block的引用;同时,为了获取其委托属性并向委托发送方法,该块必须保留对self的引用。如果应用程序中的所有其他内容都释放了对此对象的引用,则其保留计数不会为零(因为块指向它),而块没有做错任何事情(因为对象指向它),因此这一对对象将泄漏到堆中,占用内存但永远无法访问,除非使用调试器。真是悲惨。

这种情况可以通过进行以下操作轻松解决:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

在这段代码中,self 保留了 block,block 保留了 delegate,并且没有循环引用(从这里看不到。 delegate 可能会保留我们的对象,但我们目前无法控制)。这段代码不会以同样的方式冒泡风险,因为在创建 block 时捕获了 delegate 属性的值,而不是在执行时查找。一个副作用是,如果在创建此块之后更改了 delegate,则该块仍将向旧的 delegate 发送更新消息。这是否可能发生取决于您的应用程序。
即使您对这种行为感到满意,您仍然不能在您的情况下使用该技巧:
self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

在这里,你直接将 self 传递给了委托,在方法调用中,所以你必须在某个地方获取它。如果你对块类型的定义有控制权,最好的方法是将委托作为参数传递到块中:

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

这个解决方案避免了保留环的问题,并且始终调用当前代理。

如果您无法更改块,可以处理它。保留环是警告而不是错误的原因是,它们并不一定会给您的应用程序带来灾难。如果MyDataProcessor能够在操作完成之前释放块,而其父级尝试释放它之前,循环将被打破,一切都将得到适当的清理。如果您可以确定这一点,那么正确的做法是使用#pragma来抑制该代码块的警告(或使用每个文件的编译器标志。但不要禁用整个项目的警告)。

您还可以尝试使用类似上面的技巧,声明一个弱引用或未保留的引用,并在块中使用它。例如:

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

所有这三个都会给你一个引用,但不会保留结果,尽管它们的行为略有不同:__weak在对象释放时会尝试将引用清零;__unsafe_unretained会使你得到一个无效指针;__block实际上会添加另一层间接性,并允许你从块内更改引用的值(在这种情况下不相关,因为dp在其他地方没有使用)。
最好的方法取决于你能够更改哪些代码以及不能更改哪些代码。但希望这给你一些关于如何继续的想法。

1
太棒了!谢谢,我现在对正在发生的事情以及所有这些是如何工作有了更好的理解。在这种情况下,我对一切都有控制权,所以我会根据需要重新设计一些对象。 - XJones
18
我只是路过,本来有些不同的问题,但却被吸引住了,读完后现在离开这个页面感觉自己充满了知识和酷炫感。谢谢! - Orc JMR
@benzado,这里有一个问题!假设在代码块中,“您想知道”对象是否已经消失。你怎么办呢?谢谢! - Fattie
我使用 __weak typeof(self) weakSelf = self; 来使代码更具可移植性。 - stepmuel
1
你可以看一下libextobjc,它提供了两个非常方便的宏@weakify(..)@strongify(...),让你在块中以非保留方式使用self - user816328
显示剩余6条评论

25

还有一种选择,当你确定循环将在未来被打破时,可以抑制警告:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

#pragma clang diagnostic pop

这样你就不必费神去处理 __weakself别名和显式实例变量前缀了。


8
听起来像是一种非常糟糕的做法,需要超过三行的代码才能完成,而这些代码可以用“__weak id weakSelf = self;”来替换。 - pronebird
3
通常有一个更大的代码块可以受益于被抑制的警告。 - zoul
2
除了__weak id weakSelf = self;具有根本不同的行为,而不是抑制警告之外。问题始于“……如果您确信保留周期将被打破”。 - Tim
人们经常盲目地使变量变弱,而不真正理解其后果。例如,我曾看到人们将一个对象变弱,然后在块中执行以下操作: [array addObject:weakObject]; 如果weakObject已被释放,则会导致崩溃。显然,这不如保留循环更可取。 您必须了解您的块是否实际上足够长寿以证明它需要变弱,并且还要了解您是否希望块中的操作依赖于弱对象是否仍然有效。 - mahboudz

14

对于常见的解决方案,我将其定义在预编译头文件中。避免捕获并仍然通过避免使用id来启用编译器帮助。

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

然后在代码中你可以这样做:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};

同意,这可能会在块内部引起问题。ReactiveCocoa针对此问题有另一个有趣的解决方案,它允许您在块内继续使用self@weakify(self); id block = ^{ @strongify(self); [self.delegate myAPIDidFinish:self]; }; - Damien Pontifex
@dmpontifex 这是来自 libextobjc https://github.com/jspahrsummers/libextobjc 的一个宏。 - Elechtron

11

我相信没有使用ARC的解决方案同样适用于使用__block关键字的ARC:

编辑:根据转换到ARC版本说明,使用__block存储声明的对象仍然会被保留。使用__weak(优选)或__unsafe_unretained(向后兼容)。

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

// Use this inside blocks
__block id myself = self;

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [myself.delegate myAPIDidFinish:myself];
    myself.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

没想到 __block 关键字可以避免保留它的引用。谢谢!我更新了我的综合回答。 :-) - benzado
3
根据 Apple 文档,“在手动引用计数模式下,__block id x; 的效果是不保留 x。在 ARC 模式下,__block id x; 默认会保留 x(就像所有其他值一样)。” - XJones

11

结合其他答案,这是我现在用于块中的弱自引用的代码:

__typeof(self) __weak welf = self;

我将其设置为“welf”代码片段,在方法/函数中使用,只需输入“we”即可触发,XCode代码片段


你确定吗?这个链接和clang文档似乎都认为可以并且应该使用它们来保留对对象的引用,但不是会导致保留循环的链接:https://dev59.com/UmIk5IYBdhLWcg3wfOOe - Kendall Helmstetter Gelner
从clang文档中:http://clang.llvm.org/docs/BlockLanguageSpec.html “在Objective-C和Objective-C++语言中,我们允许__weak限定符用于对象类型的__block变量。如果垃圾回收未启用,则此限定符会导致这些变量保持不被发送保留消息。” - Kendall Helmstetter Gelner
让我们在聊天中继续这个讨论 - Rob

6

警告 => "在块内捕获self很可能导致保留循环"

当您在强烈地由self保留的块内引用self或其属性时,会显示上述警告。

因此,为了避免这种情况,我们必须将其设为弱引用。

__weak typeof(self) weakSelf = self;

所以,不要使用```

```,而应该使用
blockname=^{
    self.PROPERTY =something;
}

我们应该使用。
blockname=^{
    weakSelf.PROPERTY =something;
}

注意:保留周期通常发生在两个对象相互引用且它们的引用计数都为1,但是它们的dealloc方法从未被调用。


1

-1

如果您确定您的代码不会创建保留循环,或者该循环将在以后被打破,那么消除警告的最简单方法是:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

[self dataProcessor].progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

[self dataProcessor].completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

这个方法之所以有效,是因为Xcode的分析考虑了属性的点访问方式。
x.y.z = ^{ block that retains x}

在编程中,x对y的保留(在赋值语句的左侧)和y对x的保留(在右侧)被视为具有保留。即使它们是等同于点访问的属性访问方法调用,甚至当这些属性访问方法是编译器生成的时,方法调用也不受相同的分析限制。

[x y].z = ^{ block that retains x}

只有右侧被视为创建保留(由x的y),并且不会生成保留循环警告。


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