恐怕这个问题很基础,但我认为它与许多陷入困境的 Objective-C 程序员有关。
我听说的是,由于块捕获其中引用的局部变量作为 const
副本,因此在块中使用 self
可能会导致保留周期,如果该块被复制。因此,我们应该使用 __block
来强制块直接处理 self
而不是复制它。
__block typeof(self) bself = self;
[someObject messageWithBlock:^{ [bself doSomething]; }];
而不仅仅是
[someObject messageWithBlock:^{ [self doSomething]; }];
我想知道的是:如果这是真的,有没有办法可以避免丑陋(除了使用GC)?
self
代理 this
只是为了扭转局面。在 JavaScript 中,我将我的 this
闭包称为 self
,因此感觉很好且平衡。 :)
严格来说,它是一个 const 副本这一事实与这个问题无关。块将保留创建时捕获的任何 obj-c 值。碰巧 const-copy 问题的解决方法与保留问题的解决方法相同;即,对变量使用 __block
存储类。
无论如何,要回答您的问题,这里没有真正的选择。如果您正在设计自己的基于块的 API,并且这样做是有意义的,那么您可以让块将 self
的值作为参数传入。不幸的是,这对大多数 API 来说没有意义。
请注意,引用 ivar 具有完全相同的问题。如果您需要在块中引用 ivar,请改用属性或使用 bself->ivar
。
附录:当编译为 ARC 时,__block
不再中断保留循环。如果要为 ARC 进行编译,则需要改用 __weak
或 __unsafe_unretained
。
只需使用:
__weak id weakSelf = self;
[someObject someMethodWithBlock:^{
[weakSelf someOtherMethod];
}];
更多信息:WWDC 2011 - Blocks and Grand Central Dispatch in Practice。
https://developer.apple.com/videos/wwdc/2011/?id=308
注意:如果这不起作用,您可以尝试
__weak typeof(self)weakSelf = self;
这可能很明显,但是当您知道您将获得一个 保留周期时,您只需要使用丑陋的 self
别名。如果块只是一次性的,那么我认为您可以放心地忽略 self
上的保留。不好的情况是,例如,当您将块用作回调接口时。像这儿:
typedef void (^BufferCallback)(FullBuffer* buffer);
@interface AudioProcessor : NSObject {…}
@property(copy) BufferCallback bufferHandler;
@end
@implementation AudioProcessor
- (id) init {
…
[self setBufferCallback:^(FullBuffer* buffer) {
[self whatever];
}];
…
}
在这里,API 没有多大意义,但例如在与超类通信时会有意义。我们保留了缓冲区处理程序,缓冲区处理程序保留了我们。与以下内容进行比较:
typedef void (^Callback)(void);
@interface VideoEncoder : NSObject {…}
- (void) encodeVideoAndCall: (Callback) block;
@end
@interface Foo : NSObject {…}
@property(retain) VideoEncoder *encoder;
@end
@implementation Foo
- (void) somewhere {
[encoder encodeVideoAndCall:^{
[self doSomething];
}];
}
在这些情况下,我不使用 self
别名。您确实得到了一个保留周期,但该操作是短暂的,并且该块最终会耗尽内存,从而打破循环。但我对块的经验非常少,从长远来看,self
别名可能是最佳实践。
copy
,而不是 retain
。如果它们只是 retain
,则不能保证它们会从堆栈中移出,这意味着当你去执行它时,它就不再存在了。 (并且复制和已经复制的块被优化为保留)
retain
阶段并很快意识到你在说什么:)谢谢!
retain
对于块完全被忽略(除非它们已经用 copy
移出堆栈)。
发布另一个答案,因为这对我来说也是一个问题。我最初认为我必须在块内有自引用的任何地方使用 blockSelf。情况并非如此,只有当对象本身有一个块时。事实上,如果你在这些情况下使用 blockSelf 对象可以在你从块中取回结果之前被释放,然后当它试图调用它时它会崩溃,所以很明显你希望 self 被保留直到响应回来。
第一种情况演示了何时会发生保留循环,因为它包含在块中引用的块:
#import <Foundation/Foundation.h>
typedef void (^MyBlock)(void);
@interface ContainsBlock : NSObject
@property (nonatomic, copy) MyBlock block;
- (void)callblock;
@end
@implementation ContainsBlock
@synthesize block = _block;
- (id)init {
if ((self = [super init])) {
//__block ContainsBlock *blockSelf = self; // to fix use this.
self.block = ^{
NSLog(@"object is %@", self); // self retain cycle
};
}
return self;
}
- (void)dealloc {
self.block = nil;
NSLog (@"ContainsBlock"); // never called.
[super dealloc];
}
- (void)callblock {
self.block();
}
@end
int main() {
ContainsBlock *leaks = [[ContainsBlock alloc] init];
[leaks callblock];
[leaks release];
}
在第二种情况下,您不需要 blockSelf,因为调用对象中没有会在您引用 self 时导致保留循环的块:
#import <Foundation/Foundation.h>
typedef void (^MyBlock)(void);
@interface BlockCallingObject : NSObject
@property (copy, nonatomic) MyBlock block;
@end
@implementation BlockCallingObject
@synthesize block = _block;
- (void)dealloc {
self.block = nil;
NSLog(@"BlockCallingObject dealloc");
[super dealloc];
}
- (void)callblock {
self.block();
}
@end
@interface ObjectCallingBlockCallingObject : NSObject
@end
@implementation ObjectCallingBlockCallingObject
- (void)doneblock {
NSLog(@"block call complete");
}
- (void)dealloc {
NSLog(@"ObjectCallingBlockCallingObject dealloc");
[super dealloc];
}
- (id)init {
if ((self = [super init])) {
BlockCallingObject *myobj = [[BlockCallingObject alloc] init];
myobj.block = ^() {
[self doneblock]; // block in different object than this object, no retain cycle
};
[myobj callblock];
[myobj release];
}
return self;
}
@end
int main() {
ObjectCallingBlockCallingObject *myObj = [[ObjectCallingBlockCallingObject alloc] init];
[myObj release];
return 0;
}
self
的块可能不是由于人们过度应用此修复程序。这是避免在非 ARC 代码中保留循环的一个很好的例子,感谢您的发布。
还请记住,如果您的块引用 另一个 对象,然后该对象会保留 self
,则可能会发生保留循环。
我不确定垃圾收集是否可以帮助这些保留周期。如果保留块的对象(我将其称为服务器对象)比 self
(客户端对象)的寿命更长,则在释放保留对象本身之前,块内对 self
的引用不会被视为循环。如果服务器对象的寿命远远超过其客户端,则可能存在严重的内存泄漏。
由于没有干净的解决方案,我会推荐以下解决方法。随意选择其中一个或多个来解决您的问题。
仅将块用于完成,而不用于开放式事件。例如,为 doSomethingAndWhenDoneExecuteThisBlock: 之类的方法使用块,而不是 setNotificationHandlerBlock 之类的方法。用于完成的块有明确的生命周期,在评估后应该由服务器对象释放。即使保留周期发生,这也可以防止保留周期过长。
做你描述的那个弱参考舞蹈。
提供一种在释放对象之前清理对象的方法,该方法将对象与可能持有对它的引用的服务器对象“断开连接”;并在对象上调用 release 之前调用此方法。如果您的对象只有一个客户端(或者在某些上下文中是单例),则此方法非常好,但如果它有多个客户端,则会崩溃。您基本上在这里击败了保留计数机制;这类似于调用 dealloc 而不是 release。
如果您正在编写服务器对象,请仅将块参数用于完成。不接受回调的块参数,例如 setEventHandlerBlock:
。相反,回退到经典的委托模式:创建一个正式的协议,并宣传一个 setEventDelegate:
方法。不要保留代表。如果您甚至不想创建正式协议,请接受选择器作为委托回调。
最后,这种模式应该敲响警钟:
- (void)dealloc { [myServerObject releaseCallbackBlocksForObject:self]; ... }
如果您尝试从 dealloc
内部解开可能引用 self
的块,那么您已经遇到了麻烦。由于块中的引用导致的保留周期,可能永远不会调用 dealloc
,这意味着您的对象只会泄漏,直到服务器对象被释放。
__weak
,GC 确实会有所帮助。
Kevin's post 中建议的 __block __unsafe_unretained
修饰符可能会在不同线程中执行块的情况下导致错误访问异常。最好只对 temp 变量使用 __block 修饰符,并在使用后将其设为 nil。
__block SomeType* this = self;
[someObject messageWithBlock:^{
[this doSomething]; // here would be BAD_ACCESS in case of __unsafe_unretained with
// multithreading and self was already released
this = nil;
}];
您可以使用 libextobjc 库。它非常流行,例如在 ReactiveCocoa 中使用。 https://github.com/jspahrsummers/libextobjc
它提供了 2 个宏 @weakify 和 @strongify,所以你可以拥有:
@weakify(self)
[someObject messageWithBlock:^{
@strongify(self)
[self doSomething];
}];
这可以防止直接的强引用,因此我们不会进入 self 的保留周期。而且,它可以防止 self 中途变为 nil,但仍会适当地减少保留计数。此链接中的更多内容:http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakself.html
这个怎么样?
- (void) foo {
__weak __block me = self;
myBlock = ^ {
[[me someProp] someMessage];
}
...
}
我不再收到编译器警告。
块:会发生保留循环,因为它包含块中引用的块;如果您制作块副本并使用成员变量,self 将保留。
__weak
也很好。如果您知道在调用块时对象不能超出范围,那么__unsafe_unretained
会稍微快一些,但通常不会有什么不同。如果您确实使用__weak
,请确保将其放入一个__strong
局部变量中并测试它是否为非nil
,然后再对其进行任何操作。__block
不保留和释放的副作用纯粹是由于无法正确推理。使用 ARC,编译器获得了这种能力,因此__block
现在保留和释放。如果需要避免这种情况,则需要使用__unsafe_unretained
,它指示编译器不对变量中的值执行任何保留或释放。