`self`与块中的保留循环问题

169

我很抱歉,这个问题可能相当基础,但我认为它与很多进入块编程的Objective-C程序员相关。

我听说由于块捕获其中引用的本地变量作为const副本,因此在块内使用self可能会导致循环引用,如果该块被复制。因此,我们应该使用__block来强制块直接处理self,而不是将其复制。

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

不仅仅是

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

我想知道的是:如果这是真的,除了使用GC之外,是否有办法避免这种丑陋的情况?


3
我喜欢把我的self代理称为this,这样可以颠倒一下。在JavaScript中,我把我的this闭包称为self,这样感觉更加平衡。 :) - devios1
我想知道如果我正在使用Swift块,是否需要执行任何等效操作。 - Ben Lu
@BenLu 当然没问题!在Swift中,闭包(以及传递了self的函数)会保留self。有时这是需要的,但有时它会创建一个循环引用(因为闭包本身被self所拥有,或者被self拥有的某个东西所拥有)。这种情况主要是由于ARC引起的。 - Ethan
1
为了避免问题,在块中定义要使用的'self'的适当方式是 '__typeof(self) __weak weakSelf = self;',以便具有弱引用。 - XLE_22
9个回答

172
严格来说,它是一个const复制品与这个问题无关。当创建块时,块将保留任何被捕获的obj-c值。碰巧的是,用于解决const-copy问题的解决方法与解决保留问题的解决方法相同;即使用__block存储类来存储变量。
无论如何,回答你的问题,这里没有真正的替代方案。如果你正在设计自己的基于块的API,并且有意义这样做,你可以将self的值作为参数传递给块。不幸的是,对于大多数API而言,这并没有意义。
请注意,引用ivar具有完全相同的问题。如果您需要在块中引用ivar,请改用属性或使用bself->ivar
补充说明:在编译ARC时,__block 不再导致保留循环。如果您正在编译ARC,则需要改用__weak__unsafe_unretained

4
没问题,Kevin。Stack Overflow的延迟会阻止你立即选择一个问题的答案,所以我稍后回来了。干杯。 - Jonathan Sterling
@KevinBallard:实际上,如果我这样做,就会出现一个错误,说self“已经在它上面设置了保留属性”(__strong)。 - caleb
4
@JKLaiho:当然,__weak也可以。如果您确定在调用块时对象不会超出作用域,则__unsafe_unretained略微更快,但一般情况下这不会有任何区别。如果您使用__weak,请确保将其放入__strong本地变量中,并在执行任何操作之前对其进行非nil测试。 - Lily Ballard
2
@Rpranata:是的。__block不保留和释放的副作用纯粹是由于无法正确推理。有了ARC,编译器获得了这种能力,因此__block现在会保留和释放。如果您需要避免这种情况,则需要使用__unsafe_unretained,它指示编译器不对变量中的值执行任何保留或释放操作。 - Lily Ballard
@KevinBallard 在楼主的例子中,有没有可能块被self(直接或间接地)保留?我认为只有在块作为strong属性存储在self中时,块才可能被self保留(这似乎不是这种情况)。还有其他任何方式可以使self直接或间接地保留该块吗? - ken
显示剩余11条评论

68

只需使用:

__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
你可以在这里查看视频 - https://developer.apple.com/videos/wwdc/2011/#blocks-and-grand-central-dispatch-in-practice - nswamy
你能在“someOtherMethod”中引用self吗?此时self会引用weakself还是会创建一个保留循环? - Oren
嗨@Oren,如果你在“someOtherMethod”中尝试引用self,你会收到Xcode警告。我的方法只是弱引用了self。 - 3lvis
1
当我直接在块内引用self时,只会收到一个警告。将self放在someOtherMethod中不会引起任何警告。这是因为Xcode不够智能还是因为这不是问题?在someOtherMethod中引用self是否已经指向了weakSelf,因为你正在调用该方法? - Oren
显示剩余2条评论

25

可能显而易见,但只有在知道会出现保留循环时,你才需要使用难看的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];
    }];
}

在这些情况下,我不会进行自身别名。虽然会出现保留循环,但操作的持续时间很短,而且块最终会从内存中释放,打破循环。但是我的块经验非常小,也许自身别名最终会成为最佳实践。


6
好的,只有当self保持block存活时才会出现保留循环。对于从未被复制过或具有有限保证期限(例如UIView动画的完成块)的块,您不必担心保留循环问题。 - Lily Ballard
6
原则上,你是正确的。然而,如果你执行示例中的代码,程序将会崩溃。块属性应该始终声明为“copy”,而不是“retain”。如果它们只是被“retain”,那么不能保证它们会从堆栈中移除,这意味着当你去执行它时,它将不再存在。(并且对已经复制的块进行复制操作会被优化为retain) - Dave DeLong
啊,没错,是个打字错误。我之前经历过“保留”阶段,很快就意识到了你所说的事情 :) 谢谢! - zoul
我非常确定对于块而言,retain 被完全忽略了(除非它们已经通过 copy 移出了栈)。 - Steven Fisher
@Dave DeLong,不会崩溃,因为@property(retain)仅用于对象引用,而不是块。这里根本不需要使用copy。 - DeniziOS

20

我发表另一个答案,因为这对我也是个问题。一开始我认为在块内部的任何self引用处都必须使用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代码中出现保留周期的例子,感谢您的发布。 - Carl Veazey

9
请记住,如果您的块引用另一个对象,然后该对象保留self,则可能会发生保留循环。
我不确定垃圾回收对这些保留循环有所帮助。如果保留块的对象(我将其称为服务器对象)比self(客户端对象)存在更久,则在释放保留对象本身之前,块中对self的引用不会被视为循环引用。如果服务器对象远远超过其客户端,则可能会导致显著的内存泄漏。
由于没有清洁的解决方案,我建议采用以下解决方法。随意选择其中一个或多个来解决您的问题。
  • 仅为完成使用块,而不是用于开放式事件。例如,对于如doSomethingAndWhenDoneExecuteThisBlock:这样的方法,请使用块,而不是setNotificationHandlerBlock:这样的方法。用于完成的块具有明确的生命周期,并且应在它们被评估后由服务器对象释放。即使出现保留循环,这也可以使保留循环的生命周期不会太长。
  • 执行您描述的弱引用操作。
  • 提供在释放对象之前清除对象的方法,并“断开”对象与可能持有对其的引用的服务器对象之间的联系;并在调用对象上的释放方法之前调用此方法。如果您的对象只有一个客户端(或在某些情况下是单例),则此方法完全可以,但如果它具有多个客户端,则会崩溃。在这里,您基本上打败了保留计数机制;这类似于调用dealloc而不是release
如果您正在编写服务器对象,请仅为完成采用块参数。不要接受回调的块参数,例如setEventHandlerBlock:。而是退回到经典的委托模式:创建一个正式协议,并广告setEventDelegate:方法。不要保留委托。如果您甚至不想创建正式协议,请接受作为委托回调的选择器。
最后,这种模式应该引起警报:
- (void)dealloc {
    [myServerObject releaseCallbackBlocksForObject:self];
    ...
}
如果您正在尝试从dealloc内部取消连接可能引用self的块,则已经有问题。由于块中的引用引起保留循环,因此可能永远不会调用dealloc,这意味着您的对象将一直泄漏,直到服务器对象被解除分配为止。

如果适当使用__weak,GC确实会有所帮助。 - tc.
跟踪垃圾回收当然可以处理保留循环。保留循环只是引用计数环境的问题。 - newacct
只是让大家知道,在OS X v10.8中,垃圾回收被弃用,转而采用自动引用计数(ARC),并计划在未来的OS X版本中将其删除(http://developer.apple.com/library/mac/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html)。 - Ricardo Sanchez-Saez

1
你可以使用libextobjc库。它非常流行,例如在ReactiveCocoa中使用。 https://github.com/jspahrsummers/libextobjc 它提供了两个宏@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

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

1

__block __unsafe_unretained 修饰符建议在 Kevin's post 中使用,但在不同线程中执行块时可能会导致访问异常。最好只使用 __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是否更安全,以避免在使用后需要将变量置空呢? 我的意思是,如果您想要打破其他类型的循环,那么这个解决方案非常好,但对于“self”保留周期,我确实没有看到任何特殊优势。 - Alejandro Benito-Santos
如果你的平台目标是iOS 4.x,就不能使用__weak。有时候你需要在块中执行代码时,它必须针对有效对象而不是nil。 - b1gbr0

0

这个怎么样?

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

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

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


-1

Block: 包含一个在 block 中被引用的 block,将会导致循环引用; 如果你复制这个 block 并使用成员变量,self 将会被 retain。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接