一、使用NSTimer你需要了解的内容
(1)只有将计时器放在运行循环中,它才能正常的触发任务。
(2)NSTimer对象会保留target,直到计时器失效,调用invalidate可令其失效;一次性计时器触发完就失效
(3)反复执行的timer容易造成保留环。
(4)可以使用分类,用block打破保留环,后面会具体介绍
iOS 10之后引入新方法,可以得到timer弱引用避免保留环
__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
MyClass* _Nullable strongSelf = weakSelf;
[strongSelf doSomething];
}];
(5)NSTimer的实时性
无论是单次执行的NSTimer还是重复执行的NSTimer都不是准时的,这与当前NSTimer所处的线程有很大的关系,如果NSTimer当前所处的线程正在进行大数据处理(假设为一个大循环),NSTimer本次执行会等到这个大数据处理完毕之后才会继续执行。
这期间有可能会错过很多次NSTimer的循环周期,但是NSTimer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。
无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次NSTimer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。
二、相关API介绍
//使用下面两个方法创建timer,你需要手动显示将timer添加进自定义的runloop中
timerWithTimeInterval:invocation:repeats:
timerWithTimeInterval:target:selector:userInfo:repeats
//使用下面两个函数,timer会默认添加到当前runloop或者默认runloop中
scheduledTimerWithTimeInterval:invocation:repeats:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
//这里分析最后一个函数:这个方法创建出来的计时器可以在一段时间后执行任务,也可以令其重复执行任务;计时器会自动保留对象,等到任务执行完毕再释放对象。调用invalidate可以使timer失效;执行完任务后,一次性的timer会失效。如果将计时器设置为重复执行的模式,必须手动调用invalidate使其停止
//触发timer
- (void)fire;
//使timer无效
(void)invalidate;
@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;
//下面是iOS10之后引入的方法
+ (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));
+ (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));//添加到当前runloop的默认model中
- (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));
三、简单使用
@interface
NSTimer *autoTimer;
@implementation
// Start timer
autoTimer = [NSTimer scheduledTimerWithTimeInterval:(3.0)
target:self
selector:@selector(autoTimerFired:)
userInfo:nil
repeats:YES];
// Stop timer:
[autoTimer invalidate];
autoTimer = nil;
四、timer与runloop
(1)timer为什么要加入runloop?
NSTimer其实也是一种资源,如果看过多线程变成指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会又效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。你可能会说那我们APP的主线程的runloop我们没有往其中添加任何资源,为什么它还好好的运行。我们不添加,不代表框架没有添加,如果有兴趣的话你可以打印一下main thread的runloop,你会发现有很多资源。
NSTimer实例总是需要在运行循环上进行调度才能正常运行。如果您从主线程中执行此操作,则可以使用scheduleTimerWithTimeInterval,它将自动添加到主运行循环,无需手动调用NSRunLoop方法addTimer。如果需要的话,可以创建计时器并自行添加。
如果您从后台线程创建一个没有自己的运行循环的计时器(默认情况下,当您使用后台调度队列或运行队列时,正在运行的线程将不具有自己的运行循环)然后您必须手动将计时器添加到运行循环。
或者,如果您真的希望计时器在后台线程上运行,而不是为该线程创建运行循环并将计时器添加到新的运行循环,则可以使用GCD调度计时器,它不需要runloop。有关Objective-C示例
简单的英文大家应该可以理解的……^_^
If you want a repeating timer to be invoked on a dispatch_queue_t, use dispatch_source_create with a DISPATCH_SOURCE_TYPE_TIMER:
dispatch_queue_t queue = dispatch_queue_create("com.firm.app.timer", 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 20ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
// stuff performed on background queue goes here
NSLog(@"done on custom background queue");
// if you need to also do any UI updates, synchronize model updates,
// or the like, dispatch that back to the main queue:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"done on main queue");
});
});
dispatch_resume(timer);
That creates a timer that runs once every 20 seconds (3rd parameter to dispatch_source_set_timer), with a leeway of a one second (4th parameter to dispatch_source_set_timer).
To cancel this timer, use dispatch_source_cancel:
dispatch_source_cancel(timer);
因此,除非在后台线程中创建定时器,否则只需使用scheduledTimerWithTimeInterval,而不必担心手动将其添加到运行循环中
(2)runloop 的mode的切换对timer的影响
以实际例子说明
// 0.没有设置runloop模式的方式
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 1.创建NSTimer
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 2.1.添加到runloop
// 把定时器添加到当前的runloop中,并选择默认运行模式
// kCFRunLoopDefaultMode == NSDefaultRunLoopMode
// 但是这种模式下如果拖拽界面,runloop会自动进入UITrackingMode,优先于定时器追踪模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 2.2.我们更改一下模式UITrackingRunLoopMode
// 当runloop的模式是UITrackingRunLoopMode时定时器才工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 2.3.还有一种runloop的mode,占位运行模式
// 就可以无论是界面追踪还是普通模式都可以运行
/**
common modes = <CFBasicHash 0x7fb7424021b0 [0x10f12f7b0]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x11002a270 [0x10f12f7b0]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x10f14fb60 [0x10f12f7b0]>{contents = "kCFRunLoopDefaultMode"}
*/
/**
NSTimer的问题,NSTimer是runloop的一个触发源,由于NSTimer是添加到runloop中使用的,所以如果只添加到default模式,会导致拖拽界面的时候runloop进入界面跟踪模式而定时器暂停运行,不拖拽又恢复的问题,这时候就应该使用runloop的NSRunLoopCommonModes模式,能够让定时器在runloop两种模式切换时也不受影响。
*/
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
五、NSTimer保留环问题
1 @interface NSTimer (EOCBlocksSupport)
2
3 + (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
4 block:(void(^)())block
5 repeats:(BOOL)repeats;
6
7 @end
8
9 @implementation NSTimer (EOCBlocksSupport)
10
11 + (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
12 block:(void(^)())block
13 repeats:(BOOL)repeats
14 {
15 return [self scheduledTimerWithTimeInterval:interval
16 target:self
17 selector:@selector(eoc_blockInvoke:)
18 userInfo:[block copy]
19 repeats:repeats];
20 }
21
22 + (void)eoc_blockInvoke:(NSTimer*)timer {
23 void (^block)() = timer.userInfo;
24 if (block) {
25 block();
26 }
27 }
在xxxViewContoler里面使用这个扩展。
-(void)startPolling {
__weak EOCClass * weakself = self;
_pollTimer = eoc_scheduledTimerWithTimeInterval:5.0
block:^ {
EOCClass *strongSelf = weakself;
//上边代码也可以用__strong来修饰强关系,这里面应该是变为autoReleaseing的了。可以保证在这个作用于范围使用。
[strongSelf doReFresh];
}
repeats:(BOOL)YES;
}
此处使用__weak还能让程序更加安全,倘若开发者在delloc的时候忘记调用invalidate,从而使定时器在运行【这里就有疑问了,xxxcontroller持有timer,当控制器回收时timer也应该引用数为0啊,原因是这里runloop还会持有一份引用。】,倘若这样的事情发生,weakself会变为nil。
下面整理的stack overflow上的问题,对于理解NSTimeryou
疑问
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];
这句代码,NSTimer将保留target造成保留环,(如果使用timer的实例为A,当指向A实例的最后一个引用移走后,A实例不会被销毁因为timer还保留着它(这时timer重复执行),而timer也不会释放因为A引用着它,所以A实例永久的存在,也就是内存泄漏了),我了解timer必须invalidated,那么我在dealloc方法中停用timer可以避免,对吗?
解答1:在dealloc中无效定时器是无用的(在这里):定时器保持对其目标的强烈引用。这意味着只要定时器保持有效,其目标将不会被释放。作为推论,这意味着定时器的目标在其dealloc方法中尝试使定时器无效是没有意义的,只要定时器有效,dealloc方法将不被调用。
针对解答2:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
具有以下作用:(i)对self有一个弱应用; (ii)读取弱引用来提供指向NSTimer的指针。 它不会产生具有弱引用的NSTimer的效果。 该代码和使用__strong引用之间的唯一区别是,如果self被释放,那么你将传递nil给定时器。
拓展---stack overflow上提供的一种使用GCD的API事项的重复执行
- (void) doSomethingRepeatedly
{
// Do it once
NSLog(@"doing something …");
// Repeat it in 2.0 seconds
__weak typeof(self) weakSelf = self;
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[weakSelf doSomethingRepeatedly];
});
}