NSTimer

  1. 创建timer的方式
#import "TimerViewController1.h"

@interface TimerViewController1 ()

@end

@implementation TimerViewController1

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 必须手动添加到runloop才会启动
    // 首次回调时间为1秒后
    NSTimer *timer1 = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timer1Event) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSRunLoopCommonModes];
    
    // 自动添加到runloop,自动启动
    // 首次回调时间为1秒后
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timer2Event) userInfo:nil repeats:YES];
    
    // 需要手动添加到runloop启动定时器,否则使用[timer3 fire]启动的话,只会回调一次
    // 首次回调时间为设置的FireDate
    NSTimer *timer3 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timer3Event) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes];
}

- (void)timer1Event {
    NSLog(@"%s", __FUNCTION__);
}

- (void)timer2Event {
    NSLog(@"%s", __FUNCTION__);
}

- (void)timer3Event {
    NSLog(@"%s", __FUNCTION__);
}

@end

上述3种方式都存在内存泄漏的问题

  1. 循环引用和内存泄漏的分析

一般的话,我们创建一个定时器持有关系如下:


循环引用

那我把target对象对 NSTimer 变为弱引用不就解决了循环持有的问题了吗? 如下:


打破循环引用

即使这样,target依然不能释放,分析如下:
runloop持有target

主线程的Runloop在程序运行期间是不会销毁的,它比self的生命周期都长,也就是runloop引用着timer,timer就不会销毁,timer引用着target,target也不会销毁。runloop间距持有了target。

  1. 中间对象解决循环引用

可以使用一个中间对象,对 NSTimer 和 target进行弱引用,这样就解决了。当 VC 销毁了,target 也会销毁,可以通过中间对象来判断 target 是否为 nil。这样的话就可以把 NSTimer 置为无效和 nil,这样也就打破了循环引用的目的了。


解决方案
// 中间对象
@interface WeakTimer : NSObject

+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

- (instancetype)initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

@end

@interface WeakTimer ()

@property (nonatomic, weak) id aTarget;
@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, assign) SEL aSelector;

@end

@implementation WeakTimer

+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
    return [[self alloc] initWithTimeInterval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo];
}

- (instancetype)initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
    self = [super init];
    if (self) {
        _aTarget = aTarget;
        _aSelector = aSelector;
        _timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(fire) userInfo:userInfo repeats:yesOrNo];
    }
    return self;
}

- (void)dealloc {
    NSLog(@"WeakTimer dealloc");
}

- (void)fire {
    if (_aTarget) {
        [_aTarget performSelector:_aSelector];
    } else {
        [_timer invalidate];
        _timer = nil;
    }
}

@end
// 使用
@interface TimerViewController ()

@property (nonatomic, weak) WeakTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = UIColor.whiteColor;
    
    _timer = [WeakTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(callBack) userInfo:nil repeats:YES];
}

- (void)dealloc {
    NSLog(@"TimerViewController dealloc");
}

- (void)callBack {
    NSLog(@"callBack");
}

@end

在iOS 10以后系统,苹果针对NSTimer进行了优化,使用Block回调方式,解决了循环引用问题。

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"打印了");
}];

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
     NSLog(@"** dealloc **");
}

使用这种系统的 api方法,会执行的dealloc,只要在dealloc里面进行相关取消定时器的操作就就可以了。

还有下面一种方式,这种适合 push的页面,不适应present的。

//生命周期  移除VC的时候,这种适合 push的页面,不适应Present
- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self.timer invalidate];
        self.timer = nil;
    }
}
  1. NSTimer未启动原因分析

如果当前线程是主线程的话,某些UI事件,比如UIScrollView的拖拽操作,会将Runloop切换成UITrackingRunLoopMode,这时候,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。所以为了设置一个不会被UI干扰的Timer,我们需要手动将timer的当前RunloopMode设置为NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和UITrackingRunLoopMode的结合。

  1. 注意点
  • NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子线程,需要手动 run 这个 RunLoop ;同时注意使用 invalidate 手动停止定时,否则引起内存泄漏;NSTimer的创建与撤销必须在同一个线程操作,不能跨越线程操作;
  • GCD Timer 较 NSTimer 精度高,一般用于对文件资源等定期读写操作很方便,使用时需要注意 dispatch_resume 与 dispatch_suspend 配套,并且要给 dispatch source 设置新值或者置nil,需先 dispatch_source_cancel(timer) ,否则会导致崩溃;
  • 需与显示更新同步的定时,建议 CADisplayLink ,可以省去多余计算;
  • iOS中任何定时器的精度,都只是个参考值。

iOS 中精确定时的常用方法
NSTimer 的正确用法你真的知道吗?

你可能感兴趣的:(NSTimer)