- 创建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种方式都存在内存泄漏的问题
- 循环引用和内存泄漏的分析
一般的话,我们创建一个定时器持有关系如下:
循环引用
那我把target对象对 NSTimer 变为弱引用不就解决了循环持有的问题了吗? 如下:
打破循环引用
即使这样,target依然不能释放,分析如下:
runloop持有target
主线程的Runloop在程序运行期间是不会销毁的,它比self的生命周期都长,也就是runloop引用着timer,timer就不会销毁,timer引用着target,target也不会销毁。runloop间距持有了target。
- 中间对象解决循环引用
可以使用一个中间对象,对 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;
}
}
- NSTimer未启动原因分析
如果当前线程是主线程的话,某些UI事件,比如UIScrollView的拖拽操作,会将Runloop切换成UITrackingRunLoopMode,这时候,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。所以为了设置一个不会被UI干扰的Timer,我们需要手动将timer的当前RunloopMode设置为NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和UITrackingRunLoopMode的结合。
- 注意点
- NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子线程,需要手动 run 这个 RunLoop ;同时注意使用 invalidate 手动停止定时,否则引起内存泄漏;NSTimer的创建与撤销必须在同一个线程操作,不能跨越线程操作;
- GCD Timer 较 NSTimer 精度高,一般用于对文件资源等定期读写操作很方便,使用时需要注意 dispatch_resume 与 dispatch_suspend 配套,并且要给 dispatch source 设置新值或者置nil,需先 dispatch_source_cancel(timer) ,否则会导致崩溃;
- 需与显示更新同步的定时,建议 CADisplayLink ,可以省去多余计算;
- iOS中任何定时器的精度,都只是个参考值。
网友评论