实现 API 时如何避免在块中捕获自我?

Posted

技术标签:

【中文标题】实现 API 时如何避免在块中捕获自我?【英文标题】:How do I avoid capturing self in blocks when implementing an API? 【发布时间】:2011-12-12 19:49:34 【问题描述】:

我有一个可以工作的应用程序,我正在努力将其转换为 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 约定?

【问题讨论】:

【参考方案1】:

简答

您不应直接访问self,而应从不会保留的引用中间接访问它。 如果您没有使用自动引用计数 (ARC),您可以这样做:

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) 
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];

__block 关键字标记可以在块内修改的变量(我们不这样做),但在保留块时它们不会自动保留(除非您使用 ARC)。如果您这样做,您必须确保在 MyDataProcessor 实例被释放后没有其他东西会尝试执行该块。 (鉴于您的代码结构,这应该不是问题。)Read more about __block

如果您使用 ARC__block 的语义会发生变化,并且引用将被保留,在这种情况下,您应该改为声明 __weak

长答案

假设你有这样的代码:

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

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

这种情况可以通过这样做来轻松解决:

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

在这段代码中,self 保留了块,块保留了委托,并且没有循环(从这里可以看到;委托可能保留我们的对象,但现在我们无法控制)。这段代码不会以同样的方式发生泄漏,因为委托属性的值是在创建块时捕获的,而不是在执行时查找。副作用是,如果您在创建此块后更改委托,该块仍会向旧委托发送更新消息。这是否可能发生取决于您的应用程序。

即使你对这种行为很酷,你仍然不能在你的情况下使用这个技巧:

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 不会在其他任何地方使用)。

什么是最好的将取决于您可以更改哪些代码以及您不能更改哪些代码。但希望这能给您一些关于如何进行的想法。

【讨论】:

很棒的答案!谢谢,我对正在发生的事情以及这一切如何运作有了更好的了解。在这种情况下,我可以控制一切,因此我会根据需要重新设计一些对象。 O_O 我只是路过,遇到了一个稍微不同的问题,读起来卡住了,现在让这个页面感觉知识渊博,很酷。谢谢! 是正确的,如果由于某种原因在块执行时 dp 将被释放(例如,如果它是一个视图控制器并且它被弹出),那么行 [dp.delegate ... 将导致 EXC_BADACCESS? 持有区块的属性(例如dataProcess.progress)应该是strong还是weak 你可以看看libextobjc,它提供了两个方便的宏,叫做@weakify(..)@strongify(...),它允许你以非保留的方式在块中使用self【参考方案2】:

当您确定循环将来会被打破时,还可以选择取消警告:

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

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


#pragma clang diagnostic pop

这样您就不必胡乱使用__weakself 别名和显式 ivar 前缀。

【讨论】:

听起来是一个非常糟糕的做法,需要超过 3 行代码,可以替换为 __weak id weakSelf = self; 通常有更大的代码块可以从抑制的警告中受益。 除了 __weak id weakSelf = self; 具有与抑制警告完全不同的行为。问题以“......如果你确定保留周期会被打破”开始 人们常常盲目地使变量变弱,而没有真正了解其后果。例如,我见过人们弱化一个对象,然后在他们所做的块中:[array addObject:weakObject]; 如果弱对象已被释放,这会导致崩溃。显然,这不是保留周期的首选。您必须了解您的块实际上是否存在足够长的时间以保证弱化,以及您是否希望块中的动作依赖于弱对象是否仍然有效。【参考方案3】:

对于一个常见的解决方案,我在预编译头文件中定义了这些。通过避免使用 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 块 = ^ @strongify(self); [self.delegate myAPIDidFinish:self]; ; @dmpontifex 这是一个来自 libextobjc github.com/jspahrsummers/libextobjc的宏【参考方案4】:

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

编辑:根据Transitioning to ARC Release Notes,使用__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 关键字避免保留它的所指对象。谢谢!我更新了我的整体答案。 :-) 根据Apple docs“在手动引用计数模式下,__block id x;具有不保留x的效果。在ARC模式下,__block id x;默认保留x(就像所有其他值一样) 。”【参考方案5】:

结合其他几个答案,这就是我现在用于在块中使用类型化弱自我的方法:

__typeof(self) __weak welf = self;

我将它设置为 XCode Code Snippet,在方法/函数中带有“welf”的完成前缀,仅在输入“we”后才会触发。

【讨论】:

你确定吗?此链接和 clang 文档似乎认为两者都可以并且应该用于保留对对象的引用,而不是会导致保留周期的链接:***.com/questions/19227982/using-block-and-weak 来自 clang 文档:clang.llvm.org/docs/BlockLanguageSpec.html “在 Objective-C 和 Objective-C++ 语言中,我们允许对象类型的 __block 变量使用 __weak 说明符。如果未启用垃圾收集,此限定符会导致保留这些变量而不发送保留消息。” 让我们continue this discussion in chat。【参考方案6】:

warning => "在块内捕获 self 可能会导致一个保留循环"

当你在一个块中引用 self 或其属性时,它被 self 强烈保留,而不是上面的警告。

所以为了避免它,我们必须让它一周参考

__weak typeof(self) weakSelf = self;

所以而不是使用

blockname=^
    self.PROPERTY =something;

我们应该使用

blockname=^
    weakSelf.PROPERTY =something;

注意:保留循环通常发生在两个对象如何相互引用时,两个对象的引用计数=1,并且它们的 delloc 方法永远不会被调用。

【讨论】:

【参考方案7】:

新的方法是使用@weakify 和@strongify marco

@weakify(self);
[self methodThatTakesABlock:^ 
    @strongify(self);
    [self doSomething];
];

More Info about @Weakify @Strongify Marco

【讨论】:

【参考方案8】:

如果您确定您的代码不会创建保留循环,或者该循环稍后会中断,那么消除警告的最简单方法是:

// 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

被视为具有 y 的 x (在赋值的左侧)和 x 的 y (在右侧)的保留,方法调用不受相同的分析,即使它们是属性-与点访问等效的访问方法调用,即使这些属性访问方法是编译器生成的,所以在

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

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

【讨论】:

以上是关于实现 API 时如何避免在块中捕获自我?的主要内容,如果未能解决你的问题,请参考以下文章

如何在块外快速创建指向自身的弱指针

如何在块之间访问 NSManagedObject?

在块中调用时未发出 CLLocationManager 身份验证请求

尝试在块中保存对象时崩溃。 (CoreData 无法满足...的错误)

如何在块中使用 File.ReadAllBytes

ALAssetLibraryGroup 在块中找到相机胶卷