ChatGPT解决这个技术问题 Extra ChatGPT

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

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


C
Community

简短的回答

您不应直接访问 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
z
zoul

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

#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]; 如果弱对象已被释放,这会导致崩溃。显然,这不是保留周期的首选。您必须了解您的块实际上是否存在足够长的时间以保证弱化,以及您是否希望块中的动作依赖于弱对象是否仍然有效。
D
Damien Pontifex

对于一个常见的解决方案,我在预编译头文件中定义了这些。通过避免使用 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 的宏
T
Tony

我相信没有 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 文档“在手动引用计数模式下,__block id x; 具有不保留 x 的效果。在 ARC 模式下,__block id x; 默认保留 x(就像所有其他值一样)。”
C
Community

结合其他一些答案,这就是我现在使用的类型弱自我在块中使用:

__typeof(self) __weak welf = self;

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


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

警告=>“在块内捕获自我可能会导致保留周期”

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

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

__weak typeof(self) weakSelf = self;

所以而不是使用

blockname=^{
    self.PROPERTY =something;
}

我们应该使用

blockname=^{
    weakSelf.PROPERTY =something;
}

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


B
BananZ

执行此操作的新方法是使用 @weakify 和 @strongify marco

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

More Info about @Weakify @Strongify Marco


I
Iris Artin

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

// 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),并且不会生成保留周期警告。


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅