小编典典

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

all

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


阅读 60

收藏
2022-06-13

共1个答案

小编典典

简短的回答

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 保留了对块的引用;同时,该块必须保留对 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能够在操作完成时释放块,在其父级尝试释放它之前,循环将被打破,一切都会被正确清理。如果您可以确定这一点,那么正确的做法是使用
a#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在其他任何地方都没有使用)。

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

2022-06-13