使用块保留`self`循环

时间:2010-12-04 07:55:40

标签: objective-c memory-management objective-c-blocks

我担心这个问题非常基本,但我认为这与许多陷入障碍的Objective-C程序员有关。

我听到的是,由于块捕获在其中引用的局部变量为const个副本,因此如果要复制该块,则在块内使用self可能会导致保留周期。因此,我们应该使用__block强制阻止直接处理self而不是复制它。

__block typeof(self) bself = self;
[someObject messageWithBlock:^{ [bself doSomething]; }];

而不仅仅是

[someObject messageWithBlock:^{ [self doSomething]; }];

我想知道的是:如果这是真的,有没有办法可以避免丑陋(除了使用GC)?

9 个答案:

答案 0 :(得分:167)

严格地说,它是一个const副本与这个问题无关。块将保留创建时捕获的任何obj-c值。恰好,const-copy问题的解决方法与保留问题的解决方法相同;即,使用__block存储类作为变量。

无论如何,要回答你的问题,这里没有真正的选择。如果您正在设计自己的基于块的API,并且这样做有意义,您可以将块作为参数传递给self的值。不幸的是,这对大多数API都没有意义。

请注意,引用ivar具有完全相同的问题。如果您需要在块中引用ivar,请使用属性,或使用bself->ivar


附录:当编译为ARC时,__block不再中断保留周期。如果您正在为ARC进行编译,则需要使用__weak__unsafe_unretained代替。

答案 1 :(得分:63)

只需使用:

__weak id weakSelf = self;

[someObject someMethodWithBlock:^{
    [weakSelf someOtherMethod];
}];

更多信息:WWDC 2011 - 实践中的阻止和大中央调度

https://developer.apple.com/videos/wwdc/2011/?id=308

注意:如果这不起作用,您可以尝试

__weak typeof(self)weakSelf = self;

答案 2 :(得分:22)

这可能很明显,但是当你知道你会得到一个保留周期时,你只需要做一个丑陋的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别名可能是最好的做法。

答案 3 :(得分:19)

发布另一个答案,因为这对我来说也是一个问题。我原本以为我必须在块内部有自引用的任何地方使用blockSelf。情况并非如此,只有当对象本身有一个块时才会出现。事实上,如果你在这些情况下使用blockSelf,那么在你从块中获得结果之前,对象可以被释放,然后当它试图调用它时它会崩溃,所以很明显你希望自己被保留直到响应回来。

第一种情况说明何时会发生保留周期,因为它包含块中引用的块:

#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;
} 

答案 4 :(得分:9)

还要记住,如果您的块引用另一个对象,然后保留self,则会发生保留周期。

我不确定垃圾收集是否有助于这些保留周期。如果保留块的对象(我称之为服务器对象)比self(客户端对象)更长,则块中self的引用将不会被视为循环,直到保留对象本身为止释放。如果服务器对象远远超过其客户端,则可能会导致严重的内存泄漏。

由于没有干净的解决方案,我建议采用以下解决方法。您可以随意选择其中一个或多个来解决问题。

  • 仅将块用于完成,而不是用于开放式事件。例如,对doSomethingAndWhenDoneExecuteThisBlock:等方法使用块,而不使用setNotificationHandlerBlock:等方法。用于完成的块具有明确的生命终结,并且应在评估后由服务器对象释放。即使发生这种情况,这也可以防止保留周期过长。
  • 做你所描述的弱参考舞蹈。
  • 提供一种在对象释放之前清理对象的方法,该对象将对象与可能包含对象的服务器对象“断开连接”;并在调用对象上的release之前调用此方法。虽然如果您的对象只有一个客户端(或者在某些上下文中是单例),但这种方法非常好,但如果它有多个客户端则会崩溃。你基本上打败了保留计数机制;这类似于调用dealloc而不是release

如果您正在编写服务器对象,请仅使用块参数完成。不接受回调的块参数,例如setEventHandlerBlock:。相反,回到经典委托模式:创建正式协议,并宣传setEventDelegate:方法。不要保留代表。如果您甚至不想创建正式协议,请接受选择器作为委托回调。

最后,这种模式会响起警报:

- (void)dealloc {
    [myServerObject releaseCallbackBlocksForObject:self];
    ...
}

如果您尝试从self内部解除可能引用dealloc的阻止,那么您已经遇到了麻烦。由于块中引用引起的保留周期,可能永远不会调用dealloc,这意味着在释放服务器对象之前,您的对象只会泄漏。

答案 5 :(得分:1)

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;
}];

答案 6 :(得分:1)

您可以使用libextobjc库。它很受欢迎,例如在ReactiveCocoa中使用它。 https://github.com/jspahrsummers/libextobjc

它提供了2个宏@weakify和@strongify,因此你可以拥有:

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

这可以防止直接强引用,因此我们不会进入自我保留周期。而且,它会阻止self在中途变为零,但仍然可以正确地减少保留计数。 更多链接: http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakself.html

答案 7 :(得分:0)

这个怎么样?

- (void) foo {
     __weak __block me = self;

     myBlock = ^ {
        [[me someProp] someMessage];
     }
     ...
 }

我不再收到编译器警告了。

答案 8 :(得分:-1)

阻止:将发生保留周期,因为它包含块中引用的块; 如果您进行块复制并使用成员变量,则self将保留。