关于C#:在实现API时,如何避免在块中捕获self?

How do I avoid capturing self in blocks when implementing an API?

我有一个正在运行的应用程序,我正在把它转换成Xcode4.2中的ARC。其中一个预检查警告包括在导致保留周期的块中强烈捕获self。我制作了一个简单的代码示例来说明这个问题。我相信我理解这意味着什么,但我不确定实现这种类型场景的"正确"或推荐方法。

  • self是类myapi的一个实例
  • 下面的代码被简化为只显示与我的问题相关的对象和块之间的交互。
  • 假设myapi从远程源获取数据,mydataprocessor处理该数据并生成一个输出
  • 处理器配置了块来通信进度和状态

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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惯例?


简短回答

您不应该直接访问self,而是应该从不会被保留的引用中间接访问它。如果不使用自动参考计数(ARC),可以执行以下操作:

1
2
3
4
__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

__block关键字标记可以在块内修改的变量(我们不这样做),但在块被保留时不会自动保留这些变量(除非您使用的是arc)。如果这样做,则必须确保在释放mydataprocessor实例后,没有其他任何方法尝试执行该块。(考虑到代码的结构,这不应该是个问题。)阅读更多关于__block的内容。

如果您使用ARC,那么将保留__block的语义更改和引用,在这种情况下,您应该声明它__weak

长回答

假设您有这样的代码:

1
2
3
self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

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

这样做可以很容易地解决这个问题:

1
2
3
4
id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

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

即使你对这种行为很冷静,你仍然不能在你的案例中使用这种技巧:

1
2
3
self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

这里,您在方法调用中将self直接传递给委托,因此您必须将它放在某个地方。如果您可以控制块类型的定义,那么最好将委托作为参数传递到块中:

1
2
3
self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

此解决方案可避免保留周期,并始终调用当前委托。

如果你不能改变这个街区,你可以处理它。保留周期是一个警告,而不是一个错误,原因是它们不一定为您的应用程序拼写厄运。如果MyDataProcessor能够在操作完成时释放这些块,那么在它的父级尝试释放之前,循环将中断,所有内容都将被正确地清除。如果您能确定这一点,那么正确的做法是使用#pragma来禁止该代码块的警告。(或使用每个文件的编译器标志。但不要禁用整个项目的警告。)

您还可以使用上面类似的技巧,声明一个弱引用或不包含引用,并在块中使用这个技巧。例如:

1
2
3
4
5
6
__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实际上会添加另一个间接级别,并允许您更改值。来自块内的引用的e(在本例中不相关,因为dp不在任何其他地方使用)。

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


还有一个选项可以在您确定循环将在未来中断时抑制警告:

1
2
3
4
5
6
7
8
#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前缀进行胡闹。


对于一个常见的解决方案,我在预编译头中有这些定义。避免捕获并仍然通过避免使用id来启用编译器帮助

1
2
#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

然后在代码中,您可以执行以下操作:

1
2
3
4
5
BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};


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

1
__typeof(self) __weak welf = self;

我将其设置为Xcode代码片段,在方法/函数中的完整前缀为"welf",只在键入"we"后点击。


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

编辑:根据转换到ARC发行说明,使用__block存储声明的对象仍然保留。使用__weak(首选)或__unsafe_unretained(用于向后兼容性)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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];


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

当您将自身或其属性引用到自身强烈保留的块中时,该块将显示上述警告。

所以为了避免这种情况,我们必须把它作为一周的参考。

1
__weak typeof(self) weakSelf = self;

所以不用

1
2
3
blockname=^{
    self.PROPERTY =something;
}

我们应该使用

1
2
3
blockname=^{
    weakSelf.PROPERTY =something;
}

注意:当两个对象彼此引用时,引用计数都为1,并且永远不会调用它们的delloc方法时,通常会发生retain循环。


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

1
2
3
4
5
@weakify(self);
[self methodThatTakesABlock:^ {
    @strongify(self);
    [self doSomething];
}];

更多关于@weakify@strongify marco的信息


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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的分析考虑了属性的点访问,因此

1
x.y.z = ^{ block that retains x}

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

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

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