关于C#:使用块保留`self`循环

Retain cycle on `self` with blocks

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

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

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

而不仅仅是

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

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


严格地说,这是一份副本与这个问题无关。块将保留创建时捕获的所有obj-c值。正因为如此,const copy问题的变通方法与retain问题的变通方法相同,即使用__block存储类作为变量。

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

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

附录:当编译为ARC时,__block不再中断保留循环。如果您正在为ARC编译,则需要使用__weak__unsafe_unretained


只需使用:

1
2
3
4
5
__weak id weakSelf = self;

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

更多信息:WWDC 2011-实际中的区块和大中心调度。

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

注意:如果这不起作用,你可以试试。

1
__weak typeof(self)weakSelf = self;


这可能很明显,但您只需在知道将获得保留周期时使用难看的self别名即可。如果该块只是一个一次性的东西,那么我认为您可以安全地忽略self上的保留。例如,坏情况是将块作为回调接口。像这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef void (^BufferCallback)(FullBuffer* buffer);

@interface AudioProcessor : NSObject {}
@property(copy) BufferCallback bufferHandler;
@end

@implementation AudioProcessor

- (id) init {
    …
    [self setBufferCallback:^(FullBuffer* buffer) {
        [self whatever];
    }];
    …
}

在这里,API没有多大意义,但是在与超类通信时,它是有意义的,例如。我们保留缓冲处理程序,缓冲处理程序保留我们。与以下内容比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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别名可能是一种最佳实践。


发布另一个答案,因为这对我来说也是个问题。我原以为我必须在一个块内有自引用的地方使用block self。不是这样的,只有当对象本身有一个块在其中时。事实上,如果您在这些情况下使用block self,对象可以在从块中得到结果之前得到释放,然后当它试图调用它时,它将崩溃,所以很明显您希望保留self直到响应返回。

第一种情况演示了由于包含块中引用的块而发生保留循环的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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];
}

在第二种情况下,您不需要block self,因为调用对象中没有块,当您引用self时,它将导致保留周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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,则可能发生保留循环。

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

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

  • 只将块用于完成,而不用于开放式事件。例如,对方法(如doSomethingAndWhenDoneExecuteThisBlock:)使用块,而不是对方法(如setNotificationHandlerBlock:)使用块。用于完成的块有明确的生命终点,在对它们进行评估之后,服务器对象应该释放这些块。这可以防止保留循环在发生时存活太久。
  • 做你描述的弱参考舞蹈。
  • 提供一个方法,在对象被释放之前将其清理干净,该方法将对象与可能包含对其引用的服务器对象"断开连接";并在对对象调用释放之前调用此方法。如果对象只有一个客户机(或者在某些上下文中是一个单实例),则此方法非常好,但是如果对象有多个客户机,则此方法将崩溃。你基本上是在破坏这里的保留计数机制;这类似于调用dealloc,而不是release

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

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

1
2
3
4
- (void)dealloc {
    [myServerObject releaseCallbackBlocksForObject:self];
    ...
}

如果你试图从dealloc的内部解开可能涉及self的块,你就已经有麻烦了。由于块中的引用导致的保留周期,可能永远不会调用dealloc,这意味着在释放服务器对象之前,您的对象只会泄漏。


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

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

1
2
3
4
5
@weakify(self)
[someObject messageWithBlock:^{
   @strongify(self)
   [self doSomething];
}];

这会阻止直接的强引用,因此我们不会进入自我保留循环。而且,它还可以防止自己中途变成零,但仍然适当地减少保留计数。此链接中的更多内容:http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegic-solution-to-weakself.html


Kevin的文章中建议的__block __unsafe_unretained修饰符可能导致在不同线程中执行块时出现错误的访问异常。最好只对temp变量使用uu块修饰符,并在使用后将其设为零。

1
2
3
4
5
6
__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;
}];


这个怎么样?

1
2
3
4
5
6
7
8
- (void) foo {
     __weak __block me = self;

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

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


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