关于NSTimer使用的内存泄漏问题之主线程

关于NSTimer的使用我一直处于模棱两可的状态,刚好近期项目中用到NSTimer,所以整理了一些注意事项并分享给大家,如果有不对的地方希望大家能够及时指正,谢谢。

一共写了两篇文章,另一篇是《关于NSTimer使用的内存泄漏问题之子线程》

使用方法这里不再做介绍了,网上的文章很多,同时本文的结尾也推荐了两篇不错的文章供大家参考。

因为在使用过程中,我们碰见最多问题就是内存泄漏问题。本文的主要核心就是围绕内存泄漏问题展开的。

注:本文讨论皆默认为在主线程下,且target为ViewController;本文没有引用循环引用的概念,因为我认为在NSTimer的使用中,产生内存泄漏的原因是没有处理NSTimer与线程以及其线程内runloop的关系,并不是weak or strong的问题。
  • NSTimer运行原理的简单介绍
  • 内存泄漏如何产生的
  • 什么情况下不会内存泄漏
  • 如何解决内存泄漏的问题

Part 1. NSTImer运行的简单介绍

  • 其实NSTimer的运行原理很简单,就是两个因素:1.添加到runloop中;2.开启runloop。

  • scheduledTimerWith方法是创建并加入到当前的runloop中,所以创建时不需要再次添加到当前线程中,如果在主线程创建则可以直接跑起来(因为主线程的runloop是默认开启的),在子线程的话需要手动开启runloop。

  • timerWith则需手动加入runloop,如果在子线程创建,还需要手动开启当前子线程的runloop(因为子线程不会自动开启runloop)。

Part 2. 内存泄漏如何产生的

前提:当我们不使用block的方式,并且repeats == YES。

当timer加入到runloop后,runloop会持有timer,timer会保留其target对象也就是持有target 。
关于NSTimer使用的内存泄漏问题之主线程

所以不管用weak还是strong,timer对target都是强引用的,如果没有办法实现弱引用target,那么就必须要通过先释放timer才能够释放target。

所以在dealloc方法中去[timer invalidate],都会造成内存泄漏,无法释放。因为invalidate的作用是将timer从runloop中移除,而target需要等待timer被移除后才能够解除timer对自己的持有,才可以走dealloc,所以这种情况下,不能在dealloc中invalidate。

解决方案:一般情况下可以在viewWillDisappear或viewDidDisappear中释放,可以解除对target的持有,释放target。

Part 3. 什么情况下不会内存泄漏

刚才在第2点中提到了一个前提,这个前提就是不会发生内存泄露的情况,分为以下两种情况。

1. 通过block接口,这是苹果提供的api,为了防止循环引用,但需要iOS10.0以后的版本。

关于NSTimer使用的内存泄漏问题之主线程

/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  timeInterval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter:  ti    The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  fireDate   The time at which the timer should first fire.
/// - parameter:  interval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
2. repeats == NO,此时timer也不会持有target。

Part 4. 如何解决内存泄漏的问题

#pragma mark 1.不需要重复操作,repeats == NO(其实就是起一个可控的延时操作的作用)
// 当timer执行完任务后,系统会自动从当前线程中移除timer,所以不需要手动处理

#pragma mark 2.需要重复操作,即repeats == YES
/*
 * 首推block方式。虽然iOS原生api接口提供的block方式目前仅支持iOS10以后的版本,但不用担心,
   YYKit已经写好了替代方式,在YYKit中的NSTimer分类NSTimer_YYAdd中有block方法。
   只需在dealloc中[timer invalidate]即可。
*/
- (void)dealloc {
    [self.timer invalidate];
}
// 如果你习惯用非block的方式,那么就不能在dealloc中invalidate,如Part 2提到的解决方案.
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.timer invalidate];
}
// 或
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.timer invalidate];
}

参考文章:
《IOS定时器操作和NSTimer的各种坑》
《NSTimer的使用》