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

XJo*_*nes 222 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];
Run Code Online (Sandbox Code Playgroud)

问题:我在做什么"错误"和/或如何修改它以符合ARC惯例?

ben*_*ado 509

简短的回答

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

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}
Run Code Online (Sandbox Code Playgroud)

__block可以在块里面修改关键字标记变量(我们不这样做),而且他们没有当块被保留自动保留(除非您使用ARC).如果这样做,您必须确保在MyDataProcessor实例发布后没有其他任何东西会尝试执行该块.(考虑到代码的结构,这应该不是问题.)阅读更多信息__block.

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

答案很长

假设你有这样的代码:

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}
Run Code Online (Sandbox Code Playgroud)

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

通过这样做可以很容易地修复这种情况:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}
Run Code Online (Sandbox Code Playgroud)

在这段代码中,self保留了块,块保留了委托,并且没有循环(从这里可见;委托可以保留我们的对象,但现在不在我们手中).此代码不会以相同的方式冒泄漏的风险,因为在创建块时捕获委托属性的值,而不是在执行时查找.副作用是,如果在创建此块后更改委托,则块仍将向旧委托发送更新消息.这是否可能发生取决于您的申请.

即使你对这种行为很冷静,你仍然不能在你的情况下使用这个技巧:

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};
Run Code Online (Sandbox Code Playgroud)

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

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};
Run Code Online (Sandbox Code Playgroud)

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

如果您无法更改块,则可以处理它.保留周期是警告而不是错误的原因是它们不一定对您的应用程序造成厄运​​.如果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];
}
Run Code Online (Sandbox Code Playgroud)

上面的所有三个都会给你一个参考而不保留结果,尽管它们的行为都有点不同:__weak当对象被释放时会尝试将引用归零; __unsafe_unretained会给你留下无效的指针; __block实际上会添加另一个间接级别,并允许您从块中更改引用的值(在这种情况下不相关,因为dp在其他任何地方都没有使用).

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

  • O_O我只是路过一个略有不同的问题,阅读困难,现在让这个页面感觉一切都知识渊博,很酷.谢谢! (18认同)

zou*_*oul 25

当您肯定将来会破坏循环时,还可以选择禁止警告:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

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

#pragma clang diagnostic pop
Run Code Online (Sandbox Code Playgroud)

这样你就不必四处寻找__weak,self别名和明确的ivar前缀.

  • 听起来像一个非常糟糕的做法,需要超过3行代码,可以用__weak id weakSelf = self替换; (8认同)
  • 通常会有更大的代码块可以从抑制警告中受益. (3认同)
  • 除了`__weak id weakSelf = self;`的行为与抑制警告的行为有根本不同。问题开始于“ ...如果您肯定保留周期会中断” (2认同)

Dam*_*fex 14

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

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)
Run Code Online (Sandbox Code Playgroud)

然后在代码中你可以做:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
Run Code Online (Sandbox Code Playgroud)


Ton*_*ony 11

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

编辑:根据过渡到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];
Run Code Online (Sandbox Code Playgroud)

  • 根据Apple文档"在手动引用计数模式中,__block id x;具有不保留x的效果.在ARC模式下,__ _block id x;默认为保留x(就像所有其他值一样)." (3认同)

Ken*_*ner 11

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

__typeof(self) __weak welf = self;
Run Code Online (Sandbox Code Playgroud)

我将其设置为XCode代码片段,其方法/函数中的完成前缀为"welf",仅在键入"we"后才会出现.


Anu*_*uni 6

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

当你在自我强烈保留的区块内引用自我或其属性时,它比上面显示的警告更强.

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

__weak typeof(self) weakSelf = self;
Run Code Online (Sandbox Code Playgroud)

所以不要使用

blockname=^{
    self.PROPERTY =something;
}
Run Code Online (Sandbox Code Playgroud)

我们应该使用

blockname=^{
    weakSelf.PROPERTY =something;
}
Run Code Online (Sandbox Code Playgroud)

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