KISS

Keep It Simple Stupid

Objective-C: weird retain cycle warning

| comments

I was writing unit tests for an iOS project recently when I stumbled upon a very weird issue with a retain cycle warning from Clang. I created a minimal sample file to demonstrate it:

(retain_cycle_sample.m) download
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//
//  main.m
//  RetainCycleWarning
//
//  Created by u on 2015-04-04.
//  Copyright (c) 2015 yes. All rights reserved.
//

#import <Foundation/Foundation.h>
@import XCTest;


@interface Foo : NSObject

typedef void (^SuccessBlock)(CGFloat value);

- (void)setValue:(CGFloat)value
         success:(SuccessBlock)success;
- (void)getValueWithSuccessBlock:(SuccessBlock)success;

@end

@implementation Foo

- (void)setValue:(CGFloat)value
          success:(SuccessBlock)success {
}

- (void)getValueWithSuccessBlock:(SuccessBlock)success {
}

@end



@interface mainTests : XCTestCase

// used in multiple tests
@property (nonatomic, strong) Foo *foo;

@end

@implementation mainTests

- (void)setUp {
    [super setUp];
    self.foo = [Foo new];
}

- (void)tearDown {
    self.foo = nil;
    [super tearDown];
}

- (void)testFoo {
    [self.foo getValueWithSuccessBlock:^(CGFloat value) {
        XCTFail(@"success");
    }];

    [self.foo setValue:1.0
               success:^(CGFloat value) {
                   XCTFail(@"success");
               }];
}

@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

It’s the only file in a OS X command-line tool project. It has the Foo class with two simple methods to get and set a value, with empty implementations. Then, in a test class I create a property for a Foo object (because it’s originally used in multiple test methods), and test that the success blocks are not called. You don’t need to run the tests, because the point is this warning while building:

1
2
3
4
5
6
7
8
9
10
11
CompileC Library/Developer/Xcode/DerivedData/RetainCycleWarning-x/Build/Intermediates/RetainCycleWarning.build/Debug/RetainCycleWarning.build/Objects-normal/x86_64/main.o RetainCycleWarning/main.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
// …

RetainCycleWarning/RetainCycleWarning/main.m:62:20: warning: capturing 'self' strongly in this block is likely to lead to a retain cycle [-Warc-retain-cycles]
                   XCTFail(@"success");
                   ^~~~~~~~~~~~~~~~~~~
// …
RetainCycleWarning/RetainCycleWarning/main.m:60:6: note: block will be retained by an object strongly retained by the captured object
    [self.foo setValue:1.0
     ^~~~
1 warning generated.

Only one warning even though the get and set tests are the same — just calling XCTFail() in a success block.

The standard answer (e.g., http://stackoverflow.com/questions/14556605/capturing-self-strongly-in-this-block-is-likely-to-lead-to-a-retain-cycle) in such cases is to use this pattern:

1
2
3
4
5
__weak typeof(self) wself = self;
^() {
    typeof(self) sself = wself;
    // do something with sself
}

However, it’s not possible with XCTest macros, which hardcode self in them. A workaround could be noticing that XCTFail() calls _XCTPrimitiveFail(self, …) and use that instead, but that doesn’t look nice. And anyway, why is there only one warning, with setValue:success:, even though both implementations clearly can’t create a retain cycle?!

Experimenting with the code I found that if I change the name from setValue:success: to updateValue:success: removes the warning! It must be something in the name! The only answer I could find to this puzzle is here: http://stackoverflow.com/questions/15535899/blocks-retain-cycle-from-naming-convention. Basically, the method looks like a setter for the compiler, and it thinks the block may be captured. Fair enough, except it’s not the case here.

One way to fix this is to rename the method. What if you can’t do that? Here is my solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define ignore_retain_cycle_warning(block) ({\
    _Pragma("clang diagnostic push"); \
    _Pragma("clang diagnostic ignored \"-Warc-retain-cycles\""); \
    block(); \
    _Pragma("clang diagnostic pop"); \
})

    [self.foo setVolume:1.0
                success:^(CGFloat volume) {
                    ignore_retain_cycle_warning(^{
                        XCTFail(@"success");
                    });
                }];

So I’m ignoring the warning. I did it with a macro so that it can be used multiple times without the necessity to duplicate #pragma directives every time. I actually hadn’t known about _Pragma() before. The idea came from here: http://stackoverflow.com/questions/13826722/how-do-i-define-a-macro-with-multiple-pragmas-for-clang.

That’s it!

Comments