【问题标题】:Retain cycle on `self` with blocks使用块在“self”上保留循环
【发布时间】:2011-05-20 03:34:29
【问题描述】:

恐怕这个问题很基础,但我认为它与许多陷入困境的 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,因此感觉很好且平衡。 :)
  • 我想知道如果我使用 Swift 块是否需要执行任何等效操作
  • @BenLu 绝对!在 Swift 闭包(以及隐含或显式提及 self 的函数)将保留 self.有时这是需要的,有时它会创建一个循环(因为闭包本身被自己拥有(或被自己拥有的东西拥有)。发生这种情况的主要原因是因为 ARC。
  • 为避免出现问题,定义要在块中使用的“self”的适当方法是 '__typeof(self) __weak weakSelf = self;'为了有一个弱引用。

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


【解决方案1】:

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

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

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


附录:编译为 ARC 时,__block 不再中断保留周期。如果要为 ARC 编译,则需要改用 __weak__unsafe_unretained

【讨论】:

  • 没问题!如果这回答了您满意的问题,如果您能选择它作为您问题的正确答案,我将不胜感激。如果没有,请告诉我如何才能更好地回答您的问题。
  • 没问题,凯文。所以会延迟您立即选择问题的答案,所以我不得不稍后再回来。干杯。
  • __unsafe_unretained id bself = self;
  • @JKLaiho:当然,__weak 也可以。如果您知道在调用块时对象不能超出范围,那么__unsafe_unretained 会稍微快一些,但通常不会有什么不同。如果您确实使用了__weak,请确保将其放入__strong 局部变量中,并在对其进行任何操作之前对其进行测试以判断非nil
  • @Rpranata:是的。 __block 不保留和释放的副作用纯粹是由于无法正确推理。使用 ARC,编译器获得了这种能力,因此 __block 现在保留和释放。如果需要避免这种情况,则需要使用__unsafe_unretained,它指示编译器不对变量中的值执行任何保留或释放。
【解决方案2】:

只需使用:

__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;

【讨论】:

  • 你有没有偶然发现它:)?
  • 你可以在这里查看视频 - developer.apple.com/videos/wwdc/2011/…
  • 你能在“someOtherMethod”中引用self吗?那时 self 会引用弱自我还是会创建一个保留循环?
  • 嗨@Oren,如果您尝试在“someOtherMethod”中引用self,您会收到Xcode 警告。我的方法只是对自我进行了弱引用。
  • 我只在块内直接引用 self 时收到警告。将 self 放入 someOtherMethod 不会引起任何警告。那是因为 xcode 不够聪明还是不是问题?在 someOtherMethod 中引用 self 是否已经引用了weakSelf,因为这就是您调用该方法的原因?
【解决方案3】:

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

【讨论】:

  • 好点。如果 self 使块保持活动状态,这只是一个保留周期。对于永远不会被复制的块,或者有保证的有限持续时间的块(例如 UIView 动画的完成块),您不必担心。
  • 原则上你是对的。但是,如果您要执行示例中的代码,就会崩溃。块属性应该总是声明为copy,而不是retain。如果它们只是retain,则不能保证它们会从堆栈中移出,这意味着当你去执行它时,它不会再存在了。 (并且复制和已经复制的块被优化为保留)
  • 啊,当然,一个错字。我刚刚经历了retain 阶段,很快就意识到你在说什么:)谢谢!
  • 我很确定 retain 被块完全忽略(除非它们已经使用 copy 移出堆栈)。
  • @Dave DeLong,不,它不会崩溃,因为 @property(retain) 仅用于对象引用,而不是块。这里不需要使用副本所有..
【解决方案4】:

发布另一个答案,因为这对我来说也是一个问题。我最初认为我必须在块内有自引用的任何地方使用 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时会导致retain循环:

#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 代码中避免保留循环的一个很好的例子,感谢您的发布。
【解决方案5】:

还请记住,如果您的块引用 另一个 对象,然后该对象将保留 self,则可能会发生保留循环。

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

由于没有干净的解决方案,我会推荐以下解决方法。随意选择其中一项或多项来解决您的问题。

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

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

最后,这种模式应该敲响警钟:

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

如果您尝试从dealloc 内部解开可能引用self 的块,那么您已经遇到了麻烦。 dealloc 可能永远不会被调用,因为块中的引用导致的保留循环,这意味着您的对象只会泄漏,直到服务器对象被释放。

【讨论】:

  • 如果您适当地使用__weak,GC 确实会有所帮助。
  • 跟踪垃圾收集当然可以处理保留周期。保留周期只是引用计数环境的问题
  • 大家都知道,垃圾收集在 OS X v10.8 中被弃用,取而代之的是自动引用计数 (ARC),并计划在未来的 OS X 版本中删除 (developer.apple.com/library/mac/#releasenotes/ObjectiveC/…) .
【解决方案6】:

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

【讨论】:

  • 仅仅使用 __weak 而不是 __block 来避免在使用后对变量进行 nill 会不会更安全?我的意思是,如果你想打破其他类型的循环,这个解决方案很棒,但我当然看不到“自我”保留循环有什么特别的优势。
  • 如果您的平台目标是 iOS 4.x,则不能使用 __weak。此外,有时您需要块中的代码已针对有效对象执行,而不是针对 nil。
【解决方案7】:

您可以使用 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

【讨论】:

  • 在展示简化代码之前最好先了解它背后的内容,真正的两行代码大家应该知道。
【解决方案8】:

这个怎么样?

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

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

我不再收到编译器警告。

【讨论】:

    【解决方案9】:

    块:将发生保留循环,因为它包含块中引用的块; 如果你进行块复制并使用成员变量,self 将保留。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-11-02
      • 1970-01-01
      • 1970-01-01
      • 2014-08-28
      相关资源
      最近更新 更多