在实现API时,如何避免在块中捕获self?

时间:2011-10-21 18:49:40

标签: objective-c ios objective-c-blocks automatic-ref-counting

我有一个正在运行的应用程序,我正在努力将其转换为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 个答案:

答案 0 :(得分:509)

简短回答

您应该从不会保留的引用中间接访问它,而不是直接访问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未在其他任何地方使用)。

什么是最佳取决于您可以更改哪些代码以及哪些代码无法更改。但希望这能给你一些关于如何继续的想法。

答案 1 :(得分: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别名和明确的ivar前缀。

答案 2 :(得分: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;
};

答案 3 :(得分:11)

我相信没有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];

答案 4 :(得分:11)

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

__typeof(self) __weak welf = self;

我将其设置为XCode Code Snippet,其完成前缀为" welf"在方法/功能中,仅在键入"我们"。

之后命中

答案 5 :(得分:6)

warning => "在区块内捕获自我可能会导致保留周期"

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

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

__weak typeof(self) weakSelf = self;

所以不要使用

blockname=^{
    self.PROPERTY =something;
}

我们应该使用

blockname=^{
    weakSelf.PROPERTY =something;
}

注意:保留周期通常发生在一些两个对象相互引用时,它们都引用count = 1并且它们的delloc方法永远不会被调用。

答案 6 :(得分:1)

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

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

More Info about @Weakify @Strongify Marco

答案 7 :(得分:-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}

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

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

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