转载

iOS中几种定时器的介绍

背景

在iOS中我们经常会遇到一些延时任务的操作,这个时候选中一种合适的延时实现方案是很必要的,但是现在很多人只会直接的用,而不知道各种实现的区别。所以导致了遇到一些bug的时候,无从下手。接下来,我就在这边详细的介绍一下在iOS中几种定时器的区别和使用场景。

iOS中延时方案主要有以下三种:

*NSObject的方案

*NSTimer的方案

*GCD方案

用法介绍

NSObject延时实现

NSObject实现延时任务是,用到的是以下几个方法

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

首先我们看看文档这两个方法的介绍

Invokes a method of the receiver on the current thread using the default mode after a delay.This method sets up a timer to perform theaSelectormessage on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.If you want the message to be dequeued when the run loop is in a mode other than the default mode, use theperformSelector:withObject:afterDelay:inModes: method instead. If you are not sure whether the current thread is the main thread, you can use the performSelectorOnMainThread:withObject:waitUntilDone: or performSelectorOnMainThread:withObject:waitUntilDone:modes:method to guarantee that your selector executes on the main thread. To cancel a queued message, use the cancelPreviousPerformRequestsWithTarget: or cancelPreviousPerformRequestsWithTarget:selector:object: method. (方法调用经过一段延时后,在当前线程中使用默认模式执行方法_。此方法设置的定时器任务,是在当前线程的runloop中执行的。定时器配置为运行在默认模式(nsdefaultrunloopmode)。当定时器开启,当前线程试图将消息从runloop中取出并执行。如果当前的runloop已经启动并且在默认的模式,则执行成功。如果你想该延迟操作能在多个模式下执行,那么你可以使用 _performSelector:withObject:afterDelay:inModes:方法代替。如果你不确定是否当前线程是主线程,你可以使用performSelectorOnMainThread:withObject:waitUntilDone: 或performSelectorOnMainThread:withObject:waitUntilDone:modes:方法保证您的延时操作在主线程中执行。取消队列中的信息,使用cancelPreviousPerformRequestsWithTarget:或cancelPreviousPerformRequestsWithTarget:selector:object:方法。)

从上边的描述中我们可以看出以下关键的几点:

*该延时任务是执行在当前线程的当前的runloop中

*默认的延时任务执行模式是NSDefaultRunLoopMode

*延时任务的执行得确保当前线程的runloop是启动的。

关于最后一点,我这边验证了一下,确实如此。

- (void)viewDidLoad
{
[super viewDidLoad];
[NSThread detachNewThreadWithBlock:^{
[self performSelector:@selector(delaySome) withObject:nil afterDelay:0.00001];
//需要开启runloop,如果不开的话,delaySome方法不会被调用
@autoreleasepool {
[[NSThread currentThread] setName:@"delaySome"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}];
}
- (void)delaySome
{
NSLog(@"delaySome---%@", [NSThread currentThread]);
}

使用该方式执行任务,可以看出,任务是同步提交到当前线程的runloop中,所以当前runloop的一次运行循环中还有任务没有完成时,会先执行为执行的任务,才会执行该@selector(delaySome)任务。所以实际延迟的时间比afterDelay中指定的时间要长。

NSTimer延时实现

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
需要将得到的NSTimer实例使用addTimer:forMode:方法添加到runloop中,才能起到作用。
//repeats: 是否重复执行
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(delaySome) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

使用该方法,会默认添加到当前线程的runloop中去

使用NSTimer时,需要注意的时,target对象会持有timer这个实例,所以需要避免与target导致的相互持有,谁都销毁不掉的问题,最常见的当然就是ViewController中target为self,但调用-(void)invalidate却在delloc中。

显然,这种实现的模式也是添加到相应的线程中的runloop中的,也就会有上边NSObject延时实现的问题,还有一个就是销毁的问题,在网上看其他的写的关于这方面的,有些人说销毁timer实例必须在同一线程中,我这边测试了一下,显然并没有这个要求。

GCD方案

使用这种方案,我们比较常见的就是

//when:通过dispatch_time()或dispatch_walltime()返回时间。
//queue: 执行操作的队列
//block: 需要执行的操作
void dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);
//该方法是异步添加执行操作的。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self delaySome];
});

很显然dispatch_after实现延时任务很有优势,不用处理线程中runloop的问题,调用的对象不会被强引用。

虽然通过这种方式的有事很明显,但是他还是有一个缺点的,就是我们不能够手动把这个任务取消掉。

其实不用这种方式,我们也能够自己使用GCD实现可以取消的定时操作。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 1.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
[self delaySome];
});
dispatch_resume(timer);
//可以通过这个当时取消定时器任务
dispatch_cancel(timer);
//如果这个过程中传递的参数中有self持有的对象,[self delaySome]中的self得改成weakSlef;比如为了方便取消任务,self持有了timer;

注意:timer没有被销毁或是取消时,会一直调用延时任务,也就是repeats为yes。

其实我们可以简单的把这个gcd的过程封装成NSTimer的一样的使用效果。

正文到此结束
Loading...