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


222

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

Answers:


509

简短答案

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保留了块,block保留了委托,并且没有循环(从此处可见;委托可以保留我们的对象,但现在这已不复存在)。该代码不会以相同的方式冒着泄漏的风险,因为委托属性的值是在创建块时捕获的,而不是在执行时查找的。副作用是,如果在创建此块之后更改委托,则该块仍将向旧委托发送更新消息。这是否可能发生取决于您的应用程序。

即使您对这种行为很酷,也无法在您的情况下使用该技巧:

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在其他任何地方都没有使用)。

什么是最好的,你不能将取决于你能够改变什么代码和什么。但是希望这能给您一些有关如何进行的想法。


1
很棒的答案!谢谢,我对发生的事情以及这一切如何工作有了更好的了解。在这种情况下,我可以控制所有内容,因此可以根据需要重新构造一些对象。
XJones 2011年

18
O_O我只是遇到一个稍有不同的问题,被卡住了,现在离开此页面时,一切都变得博学而又酷。谢谢!
Orc JMR 2012年

是正确的,如果由于某种原因在块执行的那一刻dp将被释放(例如,如果它是一个视图控制器并且被弹出),那么该行将[dp.delegate ...导致EXC_BADACCESS?
peetonn

包含该块的属性(例如dataProcess.progress)应为strong还是weak
djskinner 2013年

1
您可能会看一下libextobjc,它提供了两个方便的宏@weakify(..)@strongify(...)它们被称为,并且允许您以self非保留方式在块中使用。

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前缀。


8
听起来这是非常糟糕的做法,它需要三行以上的代码,而这些代码可以替换为__weak id weakSelf = self;
Ben Sinclair 2013年

3
通常,可以从抑制的警告中受益更大的代码块。
zoul 2013年

2
除了__weak id weakSelf = self;行为与抑制警告有根本不同之外。问题开始于“ ...如果您肯定保留周期将被打破”
蒂姆

人们常常盲目地使变量变弱,而没有真正理解其后果。例如,我已经看到人们削弱了一个对象,然后又削弱了它们:[array addObject: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区块= ^ {@strongify(self); [self.delegate myAPIDidFinish:self]; };
Damien Pontifex 2014年

@dmpontifex这是libextobjc的宏github.com/jspahrsummers/libextobjc
Elechtron 2015年

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 2011年

3
根据Apple的文档,“在手动引用计数模式下,__ block id x;具有不保留x的作用。在ARC模式下,__ block id x;默认情况下保留x(与所有其他值一样)。”
XJones

11

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

__typeof(self) __weak welf = self;

我将其设置为方法/函数中带有“ welf”的完成前缀的XCode代码段,仅在键入“ we”后才能命中。


你确定吗?该链接和clang文档似乎都认为可以并且应该使用它来保留对对象的引用,但不能使用会导致保留周期的链接: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner 2015年

来自clang文档:clang.llvm.org/docs/BlockLanguageSpec.html “在Objective-C和Objective-C ++语言中,我们允许对象类型的__block变量使用__weak指定符。如果未启用垃圾回收,则此限定符将导致这些变量将被保留而不发送保留消息。”
Kendall Helmstetter Gelner'2


6

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

当您在一个自身强烈保留的块中引用self或其属性时,它会显示上述警告。

因此,为了避免这种情况,我们必须将其设为每周参考

__weak typeof(self) weakSelf = self;

所以不要使用

blockname=^{
    self.PROPERTY =something;
}

我们应该使用

blockname=^{
    weakSelf.PROPERTY =something;
}

注意:当两个对象相互引用且引用计数均为1且从未调用其delloc方法时,通常会发生保留周期。



-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的x(在分配的左侧)和y的x(在右侧)的保留,即使方法调用是属性访问方法调用,也不必进行相同的分析即使这些属性访问方法是由编译器生成的,也等效于点访问

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

仅右侧被视为正在创建保留(按x的y),并且不会生成任何保留周期警告。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.