用块保留“ self”上的循环


167

恐怕这个问题是很基本的,但是我认为它与许多进入块的Objective-C程序员有关。

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

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

而不只是

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

我想知道的是以下内容:如果是这样,是否有办法避免丑陋(除了使用GC外)?


3
我喜欢称呼我的self代理人this只是为了扭转局面。在JavaScript中,我将其称为thisclosures self,因此感觉不错且平衡。:)
devios1 2013年

我想知道如果我使用Swift块是否需要执行任何等效的操作
Ben Lu

@BenLu绝对!在Swift闭包中(以及隐式或显式地绕过该提及自我的函数)将保留自我。有时这是所希望的,而有时它会创建一个周期(因为闭包本身由自身拥有(或由自己拥有的东西拥有)。发生这种情况的主要原因是由于ARC。–
Ethan

1
为避免出现问题,定义在块中使用的“自我”的适当方法是“ __typeof(self)__weak weakSelf = self;”。为了有一个薄弱的参考。
XLE_22 '17

Answers:


169

严格来说,它是const副本这一事实与此问题无关。块将保留创建时捕获的所有obj-c值。碰巧,const复制问题的解决方法与保留问题的解决方法相同;也就是说,使用__block存储类作为变量。

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

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


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


没问题!如果这样使您满意,我将不胜感激,如果您可以选择它作为问题的正确答案。如果没有,请告诉我如何更好地回答您的问题。
莉莉·巴拉德

4
没问题,凯文。这样会使您无法立即选择问题的答案,所以我不得不稍后再回来。干杯。
乔纳森·斯特林

__unsafe_unretained id bself = self;
caleb 2012年

4
@JKLaiho:当然__weak可以。如果您知道一个事实,即在调用该块时该对象不能超出范围,则__unsafe_unretained速度会稍快一些,但总的来说不会有什么不同。如果您确实使用了__weak,请确保在执行任何操作之前将其放入__strong局部变量中并对其进行测试nil
莉莉·巴拉德

2
@Rpranata:是的。__block不保留和释放的副作用完全是由于无法对此进行适当的推理。使用ARC,编译器获得了该功能,因此__block现在得以保留和发布。如果需要避免这种情况,则需要使用__unsafe_unretained,它指示编译器不要对该变量的值执行任何保留或释放。
莉莉·巴拉德

66

只需使用:

__weak id weakSelf = self;

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

有关更多信息:WWDC 2011- 实践中的街区和Grand Central Dispatch

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

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

__weak typeof(self)weakSelf = self;

2
并且您是否偶然发现了它:)?
Tieme

2
你可以在这里查看视频- developer.apple.com/videos/wwdc/2011/...
nanjunda

您可以在“ someOtherMethod”中引用self吗?自我会在这一点上引用弱者,还是会形成保留周期?
奥伦2015年

@Oren,您好,如果您尝试在“ someOtherMethod”中引用self,则会收到Xcode警告。我的方法只是自我的一个弱参考。
3lvis

1
当直接在块内引用self时,我只有一个警告。将self放入someOtherMethod中不会引起任何警告。是因为xcode不够聪明还是不是一个问题?在someOtherMethod中引用self会因为已经调用了该方法而已经引用了weakSelf?
奥伦2015年

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,从长远来看,混叠可能是最佳实践。


6
好点子。如果self使该块保持活动状态,那只是一个保留周期。对于永远不会被复制的块或具有有限持续时间的块(例如,UIView动画的完成块),您不必担心。
莉莉·巴拉德

6
原则上,您是正确的。但是,如果要执行示例中的代码,则会崩溃。块属性应始终声明为copy,而不是retain。如果它们只是retain,则不能保证它们将被移出堆栈,这意味着当您执行它时,它将不再在那里。(并且已复制和已复制的块已优化为保留)
Dave DeLong 2010年

嗯,肯定是错字。我retain前一阵子经历了这个阶段,并很快意识到您在说的话:)谢谢!
zoul 2010年

我敢肯定,retain对于块来说,它是完全被忽略的(除非它们已经通过移出了堆栈copy)。
史蒂芬·费舍尔

@戴夫德隆,不,它不会崩溃,因为@property(保留)用于对象仅供参考,不做块..有没有必要将所有..这里使用一个副本
DeniziOS

20

发布另一个答案,因为这也是我的问题。我原本以为我必须在块中有自引用的任何地方使用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代码中发生保留周期的一个很好的示例,感谢您的发布。
卡尔·韦阿齐

9

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

我不确定垃圾回收可以帮助您保留这些周期。如果保留该块的对象(我称其为服务器对象)已过期self(客户端对象),则对self在释放该保留对象本身之前,对该块内部的将不被认为是循环的。如果服务器对象的客户端生存期很长,则可能会发生大量内存泄漏。

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

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

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

最后,此模式应发出警报:

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

如果您试图解开可能self从内部引用的块,则dealloc您已经遇到麻烦了。dealloc可能永远不会被调用,因为该块中的引用会引起保留周期,这意味着您的对象只会泄漏,直到服务器对象被释放为止。


如果使用__weak得当,GC会有所帮助。
tc。

跟踪垃圾回收当然可以处理保留周期。保留周期只是参考计数环境的一个问题
newacct 2012年

众所周知,OS X v10.8中不推荐使用垃圾收集,而推荐使用自动引用计数(ARC),并计划在OS X的未来版本中将其删除(developer.apple.com/library/mac/#releasenotes / ObjectiveC /…)。
里卡多·桑切斯·塞兹

1

__block __unsafe_unretained 如果在另一个线程中执行块,则Kevin帖子中建议的修饰符可能会导致错误的访问异常。最好仅将__block修饰符用于temp变量,并在使用后将其设置为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并不是真的更安全吗?我的意思是,如果您想打破其他类型的循环,则此解决方案非常有用,但是我当然看不到“自我”保留循环的任何特殊优势。
ale0xB 2012年

如果平台目标是iOS 4.x,则不能使用__weak。有时也需要为有效对象而不是nil执行块中的代码。
b1gbr0 2012年

1

您可以使用libextobjc库。它非常流行,例如在ReactiveCocoa中使用。 https://github.com/jspahrsummers/libextobjc

它提供2个宏@weakify和@strongify,因此您可以:

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

这会阻止直接的强引用,因此我们不会陷入自我的保留周期。而且,它可以防止自我中途失败,但仍可以适当减少保留计数。此链接中的更多信息:http : //aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakself.html


1
在显示简化代码之前,最好先了解其背后的含义,每个人都应该知道真正的两行代码。
Alex Cio

0

这个怎么样?

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

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

我不再收到编译器警告。


-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.