它可以选择不同的运行模式,方法来把Timer按照指

作者: 编程  发布:2019-09-15

最近在开发中计划实现一个滚动新闻(TableView的其中cell中实现),于是我定义了一个NSTimer定时器开实现一直滚动,但是当我发现了当我拖懂TableView时候,滚动新闻就停止了.于是我首先想到的是用GCD创建一个异步线程,将定时器放到这里,为了让定时器更准确,我在定时器后面添加了[[NSRunLoop currentRunLoop] run];(因为定时器在子线程中默认是不启动的) 之后确实实现了拖动tableView的时候,滚动新闻也滚动,但是dang当进入别的页面在返回当前页面的时候,有时会发生内存错误

定时器

一. 初始化

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

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

注:不用scheduled方式初始化的,需要手动addTimer:forMode: 将timer添加到一个runloop中。
而scheduled的初始化方法将以默认mode直接添加到当前的runloop中。

二. 触发
当定时器创建完(不用scheduled的,添加到runloop中后,该定时器将在初始化时指定的timeInterval秒后自动触发。
可以使用-(void)fire;方法来立即触发该定时器;
9159.com,注:You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.
在重复执行的定时器中调用此方法后立即触发该定时器,但不会中断其之前的执行计划;
在不重复执行的定时器中调用此方法,立即触发后,就会使这个定时器失效。

三. 停止
- (void)invalidate;
这个是唯一一个可以将计时器从runloop中移出的方法。
注:
NSTimer可以精确到50-100毫秒.
NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.

四. 使用
NSTimer的使用一般分三种情况,分别是NSRunLoopCommonModes和Timer 、NSThread和Timer以及GCD中的Timer。

iOS 中想实现方法的延迟执行和定时器功能,方法有很多,各有所长,所以在开发者,我们应该选择合适的方法。

延迟执行:

  • performSelector:afterDelay:
  • NSTimer
  • GCD的dispatch_after

定时器:

  • NSTimer
  • GCD的dispatch_source_t timer

今天学习别人的博客看到这个NSTimer的用法,我才知道我自己以前根本不知道,NSTimer有时候会无效。下面是原文的地址

-[UIViewAnimationState class]: message sent to deallocated instance 0x7fc9ec351490

NSRunLoopCommonModes和Timer

当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。
所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。
参考代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"主线程 %@", [NSThread currentThread]);
    //创建Timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback)userInfo:nil repeats:YES];
    //使用NSRunLoopCommonModes模式,把timer加入到当前Run Loop中。
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
//timer的回调方法
- (void)timer_callback
{
    NSLog(@"Timer %@", [NSThread currentThread]);
}

输出:

主线程 <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}

一、延迟执行

https://www.mgenware.com/blog/?p=459

在终端定位找到

NSThread和Timer

上面讲的NSRunLoopCommonModes和Timer中有一个问题,这个Timer本质上是在当前线程的Run Loop中循环执行的,因此Timer的回调方法不是在另一个线程的。那么怎样在真正的多线程环境下运行一个Timer呢?
可以先试试NSThread。同上,我们还是会把Timer加到Run Loop中,只不过这个是在另一个线程中,因此我们需要手动执行Run Loop(通过NSRunLoop的run方法),同时注意在新的线程执行中加入@autoreleasepool(非ARC)。
参考代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"主线程 %@", [NSThread currentThread]);
    //创建并执行新的线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
    [thread start];
}
- (void)newThread
{
    //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback)userInfo:nil repeats:YES];
    //开始执行新线程的Run Loop
    [[NSRunLoop currentRunLoop] run];
}
//timer的回调方法
- (void)timer_callback
{
    NSLog(@"Timer %@", [NSThread currentThread]);
}

输出:

主线程 <NSThread: 0x7118800>{name = (null), num = 1}
Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
01 - 使用performSelector:afterDelay:方式实现

官方:

// 默认只在主线程中执行,因为它默认添加到主runloop运行循环里面的NSDefaultRunLoopMode模式内
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

// 默认只在主线程中执行,但可以选择其它多种的运行循环模式,包括:NSDefaultRunLoopMode,NSRunLoopCommonModes(the default, modal, and event tracking modes)
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSString *> *)modes;

事例:

    /**
     *  延迟2秒后执行 doSomething 方法,受UI事件的影响,比如UIScrollView的拖动操作
     */
    [self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];

    /**
     *  延迟2秒后执行 不受UI事件的影响
     */
    [self performSelector:@selector(doSomething:) withObject:nil afterDelay:2.0 inModes:@[NSRunLoopCommonModes]];

我希望经过自己的努力学习,在将来的某一天是我自己研究学习写出文章。

rollNewCell timer] | -[NSRunLoop(NSRunLoop) run] | -[NSRunLoop(NSRunLoop) runMode:beforeDate:] | CFRunLoopRunSpecific | __CFRunLoopRun | __CFRunLoopDoTimer | __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ | __NSFireTimer | -[SQ_ScrollNewCell timerChanged:] |

GCD中的Timer

GCD中的Timer应该是最灵活的,而且是多线程的。GCD中的Timer是靠Dispatch Source来实现的。
因此先需要声明一个dispatch_source_t本地变量:

@interface ViewController () {
    dispatch_source_t _timer;
}

参考代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"主线程 %@", [NSThread currentThread]);
    // 间隔为 2 秒钟
    uint64_t interval = 2 * NSEC_PER_SEC;
    // 创建一个专门执行timer回调的GCD队列
    dispatch_queue_t queue = dispatch_queue_create("myTimerQueue", 0);
    // 创建timer
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 使用 dispatch_source_set_timer 函数设置timer 参数
    dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
    // 设置回调
    dispatch_source_set_event_handler(_timer, ^{
        NSLog(@"timer: %@", [NSThread currentThread]);
    });
    // dispatch_source默认是Suspended状态,通过dispatch_resume函数开始它
    dispatch_resume(_timer);
    // dispatch_suspend(_timer); 暂停定时器
}

输出:

主线程 <NSThread: 0x711fab0>{name = (null), num = 1}
Timer <NSThread: 0x713a380>{name = (null), num = 3}
Timer <NSThread: 0x713a380>{name = (null), num = 3}
Timer <NSThread: 0x713a380>{name = (null), num = 3}
02 - 使用NSTimer方式实现,设置repeats参数(是否重复)为NO

它可以选择不同的运行模式,方法来把Timer按照指定模式加入到Run。官方:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

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

事例:

    // 01 手动添加到runloop运行循环中
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

    // 02 由系统自动添加到运行循环中
     [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:NO];

解析:上面两种NSTimer的方法是一样的,01是手动将timer添加到普通的运行循环里面,02的scheduledTimerWithTimeInterval方式,系统默认将timer添加到Mode:NSDefaultRunLoopMode 模式中,所以当01的Mode为NSDefaultRunLoopMode,两者等效
但01方式创建timer更加灵活,它可以选择不同的运行模式,包含:NSRunLoopCommonModes


当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。

我才发现是之前nstimer定时器的问题

03 - 使用GCD的dispatch_after方式实现

官方:

dispatch_after(dispatch_time_t when,
    dispatch_queue_t queue,
    dispatch_block_t block);

事例:

    __weak typeof(self) weakSelf = self;
    // 参数1:设置什么时候开始,默认从当前时间开始
    // 参数2:设置时间间隔,多久后执行
    // 参数3:设置在哪个线程执行
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf doSomething];
    });

解析:利用GCD 的 dispatch_after 不需要考虑runloop的运行循环,同时还可以选择在哪个线程中执行操作


所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。(参考Apple文档)

经过查阅资料和别人帮助终于找到了解决方案

三者比较和总结

1.NSTimer 和 performSelector方法都是基于runloop的

2.如果在子线程使用 performSelector和scheduledTimerWithTimeInterval 将是无效的
主线程的runloop默认处于激活状态,如果想在子线程添加他们,需要自己创建子线程的runloop并手动启动

3.NSTimer 和 performSelector的创建与撤销必须在同一个线程操作

4.内存管理有潜在泄露的风险
scheduledTimerWithTimeInterval方法将target设为A对象时,A对象会持有这个timer,但同时,timer会被当前的runloop所持有,如果对象处理不当,可能会造成内存泄露。

当使用NSTimer的scheduledTimerWithTimeInterval方法时,事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。

而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行

所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合


经过我自己的实验,确实是这样,当我滑动UIScrollView的时候,NSTimer的处理时间根本没有调用,当我松手的时候,又执行了。

直接在主线程设置NSTimer

二、定时器

但是如果换成

 self.timer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timerChanged:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];//:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。
01 - 使用NSTimer方式实现,设置repeats参数(是否重复)为YES
实现方式和上面的用法一样,不在累述的

NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(Handlete) userInfo:nil repeats:YES];

  • 结论

  • 当系统启动时会默认创建一个runloop模式是NSDefaultRunLoopMode,当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的RunLoop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。

    • 所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。
  • 这个问题开启了我对runloop的学习的兴趣

02 - 使用GCD的dispatch_source_t timer方式实现
    __weak typeof(self) weakSelf = self;
    // dispatch_queue_t queue = dispatch_get_main_queue(); 主线程
    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, 0.1 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{

        [weakSelf doSomething:timer];

    });
    // 启动timer
    dispatch_resume(timer);
    // 暂停timer
    dispatch_suspend(timer);
    // 取消timer
    dispatch_source_cancel(timer);

使用注意:如果想要在dispatch_source_set_event_handler中实现对应的方法,必须引用block传过来的timer这个对象,否则无法执行.

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

这样滑动是没有影响的。

本文由9159.com发布于编程,转载请注明出处:它可以选择不同的运行模式,方法来把Timer按照指

关键词:

上一篇:没有了
下一篇:没有了