在这个块中强引用self很可能会导致保留环。

221

我该如何避免在xcode中出现这个警告。以下是代码片段:

[player(AVPlayer object) addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.1, 100)
queue:nil usingBlock:^(CMTime time) {
    current+=1;

    if(current==60)
    {
        min+=(current/60);
        current = 0;
    }

    [timerDisp(UILabel) setText:[NSString stringWithFormat:@"%02d:%02d",min,current]];///warning occurs in this line
}];

timerDisp 是该类的一个属性吗? - Tim
是的,@property(nonatomic,strong)UILabel *timerDisp; - user1845209
2
这是什么意思:player(AVPlayer对象)timerDisp(UILabel) - Carl Veazey
AVPlayer *player; UILabel *timerDisp; - user1845209
5
真正的问题是如何在不需要弱引用的情况下消除这个警告,当您知道循环引用将被打破时(例如,如果您总是在网络请求完成时清除引用)。 - Glenn Maynard
7个回答

525

在这里,self的捕获来自于您对self.timerDisp的隐式属性访问 - 您无法在将由self强烈保留的块内引用selfself上的属性。

在访问块内的timerDisp之前,您可以通过创建对self的弱引用来解决这个问题:

__weak typeof(self) weakSelf = self;
[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.1, 100)
                                     queue:nil
                                usingBlock:^(CMTime time) {
                                                current+=1;

                                                if(current==60)
                                                {
                                                    min+=(current/60);
                                                    current = 0;
                                                }

                                                 [weakSelf.timerDisp setText:[NSString stringWithFormat:@"%02d:%02d",min,current]];
                                            }];

14
尝试使用__unsafe_unretained代替。 - Tim
64
已解决。请改用以下代码:__unsafe_unretained typeof(self) weakSelf = self;感谢@Tim的帮助。 - user1845209
1
很好的回答,但我对你说“你不能在将被self强烈保留的块内引用self或self属性”这一点有些不同意。这并不是严格正确的。请看下面我的回答。最好说,“如果你引用self,你必须非常小心…” - Chris Suter
8
我认为OP的代码中不存在保留循环。该代码块并不是由self强烈保留的,而是由主调度队列保留的。我错了吗? - erikprice
3
你没错。我理解问题的重点是关于Xcode提出的错误警告(“如何避免在Xcode中出现此警告”),而不是实际存在保留循环的问题。你说得对,从OP提供的代码片段中并没有明显的保留循环。 - Tim
显示剩余11条评论

55
__weak MyClass *self_ = self; // that's enough
self.loadingDidFinishHandler = ^(NSArray *receivedItems, NSError *error){
    if (!error) {
       [self_ showAlertWithError:error];
    } else {
       self_.items = [NSArray arrayWithArray:receivedItems];
       [self_.tableView reloadData];
    }
};

还有一件非常重要的事情需要记住:不要直接在block中使用实例变量,而应该将其作为弱对象的属性使用,示例如下:

self.loadingDidFinishHandler = ^(NSArray *receivedItems, NSError *error){
        if (!error) {
           [self_ showAlertWithError:error];
        } else {
           self_.items = [NSArray arrayWithArray:receivedItems];
           [_tableView reloadData]; // BAD! IT ALSO WILL BRING YOU TO RETAIN LOOP
        }
 };

不要忘记要做:

- (void)dealloc {
    self.loadingCompletionHandler = NULL;
}

如果你传递一个没有被任何人保留的对象的弱引用,就会出现另一个问题:

MyViewController *vcToGo = [[MyViewCOntroller alloc] init];
__weak MyViewController *vcToGo_ = vcToGo;
self.loadingCompletion = ^{
    [vcToGo_ doSomePrecessing];
};

如果 vcToGo 将被释放,那么在这个块中运行时,我相信你会遇到无法识别的选择器崩溃,因为垃圾桶中包含当前 vcToGo_ 变量。尝试控制它。


3
如果您也解释一下,这个答案会更有力。 - Eric J.

51

更好的版本

__strong typeof(self) strongSelf = weakSelf;

在你的代码块中,将弱引用作为第一行创建一个强引用。如果self在代码块开始执行时仍然存在且没有回退到nil,则此行确保它在整个代码块的执行期间持续存在。
因此,整个过程应该像这样:
// Establish the weak self reference
__weak typeof(self) weakSelf = self;

[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.1, 100)
                                 queue:nil
                            usingBlock:^(CMTime time) {

    // Establish the strong self reference
    __strong typeof(self) strongSelf = weakSelf;

    if (strongSelf) {
        [strongSelf.timerDisp setText:[NSString stringWithFormat:@"%02d:%02d",min,current]];
    } else {
        // self doesn't exist
    }
}];

我已经多次阅读了这篇文章。这是由 Erica Sadun 撰写的一篇关于如何在使用块和NSNotificationCenter时避免问题的优秀文章。


Swift更新:

例如,在Swift中,一个带有成功块的简单方法将是:

func doSomeThingWithSuccessBlock(success: () -> ()) {
    success()
}

当我们调用这个方法并需要在成功的回调中使用self时,我们将会使用[weak self]guard let特性。

    doSomeThingWithSuccessBlock { [weak self] () -> () in
        guard let strongSelf = self else { return }
        strongSelf.gridCollectionView.reloadData()
    }

这种所谓的强弱舞蹈是流行的开源项目 Alamofire 使用的。

更多信息请查看swift-style-guide


如果你在块之外(而不是使用__weak)使用typeof(self) strongSelf = self;,然后在块中使用后说strongSelf = nil;会怎样呢?我不明白你的例子如何确保weakSelf在块执行时不为nil。 - Matt
为了避免可能的保留循环,我们在使用 self 的任何块之外建立一个弱引用。这样,您必须确保该块被执行。您代码中的另一个块现在负责释放先前保留的内存。 - Warif Akhand Rishi
1
@Matt,这个例子的目的不是让weakSelf被保留。目的是,如果weakSelf不为nil,在块内部建立一个强引用。因此,一旦块开始使用self执行,self在块内部不会变成nil。 - Warif Akhand Rishi
当我只有weakself时,Xcode会抱怨我的使用方式,因为它可能为空。这个答案解决了这个问题。值得更多的赞。 - auspicious99

15

在另一个答案中,Tim说:

如果在一个块内部引用self或者self上的属性并且该块由self强引用,那么将会出现循环引用。

这并不完全正确。只要你最终打破循环依赖关系即可。比如说,假设你有一个定时器在触发时会有一个保留self的块,同时你也在self中保留了定时器的强引用,那么只要你确定你会在某个时刻销毁该定时器并打破循环依赖关系,这种情况就是完全正常的。

就我自己而言,我最近为一个包含以下代码的代码产生了警告:

[x setY:^{ [x doSomething]; }];

我知道clang只有在检测到方法以“set”开头(以及另一种特殊情况,我这里不会提到)时才会产生此警告。对于我来说,我知道没有保留循环的危险,所以我将方法名称更改为“useY:”。当然,在大多数情况下,您通常会想使用弱引用,但我认为值得注意的是我的解决方案,以防对他人有所帮助。


6
很多时候,这实际上并不是一个保留循环。
如果你知道它不是,就没必要把无用的weakSelves带入这个世界。
苹果甚至通过他们的UIPageViewController API强制给我们这些警告,其中包括一个设置方法(触发这些警告-如其他地方所提到的-认为你正在给一个block类型的ivar赋值),以及一个完成处理程序块(其中你无疑会引用自己)。
以下是一些编译指令,用于消除代码中那一行的警告。
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
    [self.pageViewController setViewControllers:@[newViewController] direction:navigationDirection animated:YES completion:^(BOOL finished) {
        // this warning is caused because "setViewControllers" starts with "set…", it's not a problem
        [self doTheThingsIGottaDo:finished touchThePuppetHead:YES];
    }];
#pragma clang diagnostic pop

这是否比不必要的弱化(备忘录给我的)自己看起来更好一点? - Integer Poet

1
在提高精度和风格方面,我有两个建议。在大多数情况下,您只会在此块中使用一个或几个self成员,最有可能只是更新滑块。强制转换self太过浪费。相反,更好的方法是明确地强制转换您在块内真正需要的对象。例如,如果它是UISlider*的实例,比如说_timeSlider,只需在块声明之前执行以下操作:
UISlider* __weak slider = _timeSlider;

然后只需在块内使用slider。从技术上讲,这更加精确,因为它将潜在的保留周期仅限于您需要的对象,而不是self中的所有对象。

完整示例:

UISlider* __weak slider = _timeSlider;
[_embeddedPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 1)
     queue:nil
     usingBlock:^(CMTime time){
        slider.value = time.value/time.timescale;
     }
];

此外,最有可能被转换为弱指针的对象已经在self内作为弱指针存在,从而最大程度减少或完全消除了保留循环的可能性。在上面的示例中,_timeSlider实际上是作为弱引用存储的属性,例如:
@property (nonatomic, weak) IBOutlet UISlider* timeSlider;

就编码风格而言,与C和C++一样,变量声明最好从右到左阅读。以这种顺序声明SomeType* __weak variable更自然地从右向左阅读,即:variable是指向SomeType的弱指针

1
最近我遇到了这个警告,想更好地理解它。经过一番试错,我发现它源于一个方法以"add"或"save"开头。Objective C将以"new"、"alloc"等开头的方法名视为返回一个已保留的对象,但没有提到(我找不到)关于"add"或"save"的内容。然而,如果我以这种方式使用方法名:
[self addItemWithCompletionBlock:^(NSError *error) {            [self done]; }];

我会在[self done]这一行看到警告。然而,这不会:
[self itemWithCompletionBlock:^(NSError *error) {    [self done]; }];

我将使用 "__weak __typeof(self) weakSelf = self" 的方式引用我的对象,但实际上并不喜欢这样做,因为它会让未来的我和/或其他开发者感到困惑。当然,我也可以不使用 "add"(或 "save"),但这更糟糕,因为它会削弱方法的含义。

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