iOS开发基础16-使用 `NSTimer` 时避免内存泄露的技巧和最佳实践

在iOS开发中,NSTimer 是一个常用的工具,用于实现周期性任务。然而,在使用过程中,如果不注意管理内存,容易导致内存泄露问题,特别是当 NSTimer 针对 self 执行回调时。这篇文章将详细介绍如何通过一些技巧和封装来避免这些问题。

问题背景

以下代码创建了一个计时器,每隔3秒钟在控制台输出一次 "Fire":

@interface DetailViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end

@implementation DetailViewController

- (IBAction)fireButtonPressed:(id)sender {
    _timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
                                              target:self
                                            selector:@selector(timerFire:)
                                            userInfo:nil
                                             repeats:YES];
    [_timer fire];
}

- (void)timerFire:(id)userinfo {
    NSLog(@"Fire");
}
@end

尽管上述代码可以正常工作,但我们发现,即使跳转到其他界面,计时器依然在后台继续运行。显然,计时器没有被正确释放。

内存泄露分析

强引用的陷阱

  1. NSTimerRunLoop 强引用:当 NSTimer 被添加到 RunLoop 中时,它会被 RunLoop 强引用,从而不需要你手动保留它。

    Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
    
  2. NSTimer 强引用 target:一个更为重要的点是,NSTimer 会强引用其 target,即使 targetself

    Target is the object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
    

因此,如果 targetselfNSTimer 会保持对 self 的强引用,导致 self 不能被释放,从而不会调用 dealloc 方法来释放计时器。

解决方案

1. 手动释放 NSTimer

在视图控制器的 dealloc 方法中手动调用 invalidate 来释放计时器:

- (void)dealloc {
    [_timer invalidate];
    NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}

此方法虽然直截了当,但需要注意在合适的时机调用 invalidate,特别是计时器所在的线程。

2. 使用弱引用目标(HWWeakTimerTarget

为了避免 NSTimer 强引用 self,可以创建一个中间目标来打破这种强引用关系。

实现中间目标

首先,定义一个中间目标类:

@interface HWWeakTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
@end

@implementation HWWeakTimerTarget

- (void)fire:(NSTimer *)timer {
    if (self.target) {
        [self.target performSelector:self.selector withObject:timer.userInfo];
    } else {
        [self.timer invalidate];
    }
}

@end
定义一个新的 scheduledTimerWithTimeInterval 方法

封装一个新的方法来创建 NSTimer,通过使用 HWWeakTimerTarget

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats {
    HWWeakTimerTarget *timerTarget = [[HWWeakTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                         target:timerTarget
                                                       selector:@selector(fire:)
                                                       userInfo:userInfo
                                                        repeats:repeats];
    return timerTarget.timer;
}
使用新的方法创建 NSTimer
- (IBAction)fireButtonPressed:(id)sender {
    _timer = [HWWeakTimer scheduledTimerWithTimeInterval:3.0f
                                               target:self
                                             selector:@selector(timerFire:) 
                                             userInfo:nil
                                              repeats:YES];
    [_timer fire];
}

3. 使用 Block 方式的NSTimer

通过 Block 方式来创建 NSTimer,保持代码简洁:

实现 Block 版本的 NSTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      block:(HWTimerHandler)block
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats {
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(_timerBlockInvoke:)
                                       userInfo:@[[block copy], userInfo]
                                        repeats:repeats];
}

+ (void)_timerBlockInvoke:(NSArray *)userInfo {
    HWTimerHandler block = userInfo[0];
    id info = userInfo[1];
    if (block) {
        block(info);
    }
}
使用 Block 创建 NSTimer
- (IBAction)fireButtonPressed:(id)sender {
    _timer = [HWWeakTimer scheduledTimerWithTimeInterval:3.0f
                                                    block:^(id userInfo) {
                                                        NSLog(@"%@", userInfo);
                                                    }
                                                 userInfo:@"Fire"
                                                  repeats:YES];
    [_timer fire];
}

总结

通过上述方式,可以有效避免在使用 NSTimer 时的内存泄露问题。关键在于:

  1. 理解 NSTimerRunLoop 的强引用关系。
  2. 使用中间目标类(HWWeakTimerTarget)打破 NSTimerself 的强引用。
  3. 使用 Block 方式简化计时器的创建和管理。

这篇文章能帮助你更好地理解和使用 NSTimer,避免常见的内存泄露问题。

posted @ 2015-07-22 12:57  Mr.陳  阅读(591)  评论(0编辑  收藏  举报