使 UIViewController 中的 NSTimer 无效以避免保留周期的最佳时间

Posted

技术标签:

【中文标题】使 UIViewController 中的 NSTimer 无效以避免保留周期的最佳时间【英文标题】:Best time to invalidate NSTimer inside UIViewController to avoid retain cycle 【发布时间】:2011-03-29 13:07:29 【问题描述】:

有谁知道什么时候是停止在 UIViewController 内保持引用的 NSTimer 的最佳时间,以避免在计时器和控制器之间保持循环?

这是更详细的问题:我在 UIViewController 中有一个 NSTimer。

在视图控制器的 ViewDidLoad 期间,我启动了计时器:

statusTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateStatus) userInfo: nil repeats: YES];

以上内容导致计时器持有对视图控制器的引用。

现在我想释放我的控制器(例如父控制器释放它)

问题是:我在哪里可以调用 [statusTimer invalidate] 来强制计时器释放对控制器的引用?

我尝试将它放在 ViewDidUnload 中,但在视图收到内存警告之前它不会被触发,所以这不是一个好地方。我尝试了dealloc,但只要计时器还活着,就永远不会调用dealloc(鸡和鸡蛋问题)。

有什么好的建议吗?

【问题讨论】:

定时器中不需要保留视图控制器。 VC 应该拥有计时器,就像它拥有的任何其他对象一样,并在适当的时候销毁它。 @logancautrell,你的观点很好,但NSTimer 保留了你传递给它的目标,并且无法更改。 (不过,一些答案提出了尝试解决这个问题的方法。) 哎呀,我看到了你遇到的问题。另一种方法是添加一个您的 VC 和计时器都拥有的辅助对象。 【参考方案1】:

    您可以避免保留周期,例如,将计时器对准一个StatusUpdate 对象,该对象包含对您的控制器的非保留(弱)引用,或者通过使用StatusUpdater 用你的控制器的指针初始化,持有对它的弱引用,并为你设置计时器。

    当目标窗口为 nil(应处理您提供的 -viewDidDisappear: 的反例)以及在-viewDidDisappear:。这确实意味着您的视图正在回到您的控制器;您可以通过向控制器发送-view:willMoveToWindow: 消息或发布通知(如果您关心的话)来避免伸手去抢计时器。

    大概是您导致视图从窗口中移除,因此您可以在驱逐视图的行旁边添加一行来停止计时器。 p>

    您可以使用非重复计时器。它一触发就会失效。然后,您可以在回调中测试是否应该创建一个新的非重复计时器,如果是,则创建它。然后,不需要的保留周期将只保留计时器和控制器对,直到下一个触发日期。使用 1 秒的触发日期,您无需担心太多。

除了第一个建议之外的每一个建议都是一种适应保留周期并在适当的时间打破它的方法。第一个建议实际上避免了保留循环。

【讨论】:

我更喜欢使用非重复计时器并在回调中重新创建。 我认为使用viewWillMovetoWindow: 是使用具有计时器的自定义视图类时的最佳解决方案,并且当使用它的视图控制器(VC)不包含对该视图类的引用时。考虑在 VC 中初始化动态视图的情况;当调用viewWillDisappear: 时,VC 没有明确的方法来告诉视图类使其计时器无效。它可以遍历其视图并在其视图类上创建cleanup 方法或使用NSNotification,但我认为willMoveToWindow: 是最干净的。不要认为非重复计时器建议在这里会起作用。 @IulianOnofrei 正如 OP 所指出的,存在先有鸡还是先有蛋的问题:当计时器保留其目标时,其目标不会释放。如果它的目标期望使保留它的计时器失效,那么它永远不会使它失效,因为计时器阻止它解除分配!如果仍然不清楚,我建议提出一个新的完整问题。 @JeremyW.Sherman,但看起来情况并非如此,因为我在UIViewControllers 中使用dealloc 来释放计时器并且它工作正常。实际上,您覆盖的dealloc 不是在任何NSObject 释放之前调用吗?那么dealloc 是干什么用的呢?如果不手动释放相应对象的属性? @IulianOnofrei 请ask a question 提供一些示例源代码,以使讨论具体化。简短的回答是“因为在计时器的目标也是其所有者的常见情况下保留(又名强引用)循环”。【参考方案2】:

一种解决方法是让 NStimer 持有对 UIViewController 的弱引用。我创建了一个对您的对象进行弱引用并将调用转发给该对象的类:

#import <Foundation/Foundation.h>

@interface WeakRefClass : NSObject

+ (id) getWeakReferenceOf: (id) source;

- (void)forwardInvocation:(NSInvocation *)anInvocation;

@property(nonatomic,assign) id source;

@end

@implementation WeakRefClass

@synthesize source;

- (id)init
    self = [super init];
//    if (self) 
//    
    return self;


+ (id) getWeakReferenceOf: (id) _source

    WeakRefClass* ref = [[WeakRefClass alloc]init];
    ref.source = _source; //hold weak reference to original class

    return [ref autorelease];


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 
    return [[self.source class ] instanceMethodSignatureForSelector:aSelector];


- (void)forwardInvocation:(NSInvocation *)anInvocation

    [anInvocation    invokeWithTarget:self.source ];



@end

然后你像这样使用它:

statusTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: [WeakRefClass getWeakReferenceOf:self] selector: @selector(updateStatus) userInfo: nil repeats: YES];

您的 dealloc 方法被调用(与以前不同),您只需在其中调用:

[statusTimer invalidate];

【讨论】:

【参考方案3】:

您可以尝试使用- (void)viewDidDisappear:(BOOL)animated,然后您应该在- (void)viewDidAppear:(BOOL)animated 中再次验证它

More here

【讨论】:

当在 UINavigationController 中时,viewDidDisappear 不会被调用,所以我正在寻找一个更通用的地方来放置它,同样当我使用 [self.view removeFromSuperview] 隐藏视图时,viewDidDisappear 不会也被调用。 在 UINavigationController 内部是什么意思。当你弹出它?你能发布更多关于那个的代码吗【参考方案4】:

-viewDidDisappear 方法可能正是您想要的。每当视图被隐藏或关闭时都会调用它。

【讨论】:

但是当我使用 [self.view removeFromSuperview] 隐藏我的父视图时,永远不会调用 viewDidDisappear。还有其他建议吗? 另外,当在 UINavigationController 内部时,viewDidDisappear 不会被调用,所以我正在寻找一个更通用的地方来放置它。 viewDidDisappear 如果可以依赖它总是被调用,那将是完美的,不幸的是,情况并非如此【参考方案5】:

invalidate timer inside - (void)viewWillDisappear:(BOOL)animated 对我有用

【讨论】:

【参考方案6】:

对于 @available(iOS 10.0, *),您还可以使用:

Timer.scheduledTimer(
    withTimeInterval: 1,
    repeats: true,
    block:  [weak self] _ in
        self?.updateStatus()
    
)

【讨论】:

【参考方案7】:

正是出于这个原因,我编写了一个“弱引用”类。它是 NSObject 的子类,但会将 NSObject 不支持的所有方法转发给目标对象。定时器保留了weakref,但是weakref没有保留它的target,所以没有retain循环。

目标在dealloc中调用[weakref clear]和[timer invalidate]左右。恶心,不是吗?

(下一个显而易见的事情是编写您自己的计时器类来为您处理所有这些。)

【讨论】:

【参考方案8】:

如果 timer.REPEAT 设置为YES,则计时器的所有者(例如视图控制器或视图)在计时器无效之前不会被释放。

这个问题的解决方案是找到一些触发点来停止你的计时器。

例如,我启动一个计时器来在视图中播放动画 GIF 图像,触发点是:

    当视图添加到超级视图时,启动计时器 当视图从父视图中移除时,停止计时器

所以我选择了UIViewwillMoveToWindow:方法:

- (void)willMoveToWindow:(UIWindow *)newWindow 
    if (self.animatedImages && newWindow) 
        _animationTimer = [NSTimer scheduledTimerWithTimeInterval:_animationInterval
            target:self selector:@selector(drawAnimationImages)
            userInfo:nil repeats:YES];
     else 
        [_animationTimer invalidate];
        _animationTimer = nil;
    

如果您的计时器归 ViewController 所有,viewWillAppear:viewWillDisappear: 可能是您启动和停止计时器的好地方。

【讨论】:

【参考方案9】:

我遇到了完全相同的问题,最后我决定重写 View Controller 的 release 方法,以查找 retainCount 为 2 并且我的计时器正在运行的特殊情况。如果计时器没有运行,那么这将导致释放计数降至零,然后调用 dealloc。

- (oneway void) release 
    // Check for special case where the only retain is from the timer
    if (bTimerRunning && [self retainCount] == 2) 
        bTimerRunning = NO;
        [gameLoopTimer invalidate];
    
    [super release];

我更喜欢这种方法,因为它保持简单并封装在一个对象(即视图控制器)中,因此更易于调试。但是,我不喜欢在保留/释放链上胡闹,但我找不到解决办法。

希望这会有所帮助,如果您确实找到了更好的方法,也很想听听。

戴夫

编辑:应该是 -(oneway void)

【讨论】:

【参考方案10】:

您可以在视图控制器的dealloc函数中编写此代码

例如。

-(void)dealloc

   if([statusTimer isValid])
  
       [statusTimer inValidate];
       [statustimer release];
      statusTimer = nil;
  

这样statustimer的引用计数器会自动减1 并且分配的内存上的数据也将被删除

你也可以在- (void)viewDidDisappear:(BOOL)animated函数中编写这段代码

【讨论】:

-1 问题是dealloc 永远不会被调用,因为NSTimer 使UIViewController 保持活动状态。

以上是关于使 UIViewController 中的 NSTimer 无效以避免保留周期的最佳时间的主要内容,如果未能解决你的问题,请参考以下文章

使 UIViewController 中的 NSTimer 无效以避免保留周期的最佳时间

使 UIViewController 中的 UITableView 滚动到文本字段

如何使 PFQueryTableView 在 UIViewController 中工作

如何使 UIViewController 工具栏出现? [关闭]

在 UIViewController 中使单元格可点击

如何使 UIViewController 成为单独 UITableViewController 的委托?