前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS计时器:NSTimer

iOS计时器:NSTimer

作者头像
用户5521279
发布2020-07-16 14:21:13
1.7K0
发布2020-07-16 14:21:13
举报
文章被收录于专栏:搜狗测试搜狗测试

背景

前阵子在整理RunLoop原理的时候发现代码中用到了很多NSTimer,其中也出现了挺多问题,这里整理了一些NSTimer的使用方法供大家使用避坑。

NSTimer介绍

NSTimer的创建通常有两种方式,一种是以scheduledTimerWithTimeInterval 为开头的类方法 。这些方法在创建了NSTimer 之后会将这个 NSTimer 以NSDefaultRunLoopMode 模式放入当前线程的 RunLoop。

代码语言:javascript
复制
+ ( NSTimer *) scheduledTimerWithTimeInterval:invocation:repeats: 
+ ( NSTimer *) scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

另一种是以 timerWithTimeInterval 为开头的类方法。这些方法创建的NSTimer 并不能马上使用,还需要调用 RunLoop 的addTimer:forMode: 方法将 NSTimer 放入 RunLoop,这样 NSTimer 才能正常工作。

代码语言:javascript
复制
 + ( NSTimer *) timerWithTimeInterval:invocation:repeats:
 + ( NSTimer *) timerWithTimeInterval:target:selector:userInfo:repeats:

NSTimer常见问题

1. 循环引用

先看个例子:

代码语言:javascript
复制
@interface TimerViewController ()

@property (nonatomic,strong) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES];
}

-(void)outputLog:(NSTimer *)timer{
    NSLog(@"it is log!");
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}
@end

上面这段代码中TimerViewController和NSTimer构成了循环引用,退出TimerViewController页面,TimerViewController 和 NSTimer 都无法释放,TimerViewController 的 dealloc 方法没有被调用,NSTimer 就没有被 invalidate 。

那如果 TimerViewController 弱引用一个 NSTimer,是不是能够解决这个问题呢?

代码语言:javascript
复制
@interface TimerViewController ()
// 使用 weak
@property (nonatomic,weak) NSTimer *timer;

@end
// ...... 省略代码

很不幸,结果应该和上面是一样的,那为什么呢?

TimerViewController 需要 NSTimer 同生共死。NSTimer 需要在 TimerViewController 的 dealloc 方法被 invalidate 。NSTimer 被 invalidate 的前提是 TimerViewController 被 dealloc。而 NSTimer 一直强引用着 TimerViewController 导致 TimerViewController 无法调用 dealloc 方法。那我们的解决方案就是要让NSTimer不持有TimerViewController,也就是说NSTimer的target对象不是TimerViewController。我们这里有两个方案:

① 将 target 分离出来独立成一个 WeakProxy 代理对象, NSTimer 的 target 设置为 WeakProxy 代理对象,WeakProxy 是 TimerViewController 的代理对象,所有发送到 WeakProxy的消息都会被转发到 TimerViewController 对象。使用代理对象可以达到 NSTimer 不直接持有 TimerViewController 的目的。

代码语言:javascript
复制
#import "TimerViewController.h"
#import "YYWeakProxy.h"
@interface TimerViewController ()

@property (nonatomic,weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[YYWeakProxy proxyWithTarget:self] selector:@selector(outputLog:) userInfo:nil repeats:YES];
}

- (void)outputLog:(NSTimer *)timer{
    NSLog(@"it is log!");
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}


@end

② 通过category 把 NSTimer 的 target 设置为 NSTimer 类,让 NSTimer 自身做为target, 把 selector 通过 block 传入给 NSTimer,在NSTimer 的 category 里面触发selector 。

代码语言:javascript
复制
// NSTimer+BlocksSupport.h
#import <Foundation/Foundation.h>

@interface NSTimer (BlocksSupport)
+ (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       repeats:(BOOL)repeats
                                         block:(void(^)())block;
@end

// NSTimer+BlocksSupport.m
#import "NSTimer+BlocksSupport.h"

@implementation NSTimer (BlocksSupport)
+ (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       repeats:(BOOL)repeats
                                         block:(void(^)())block;
{
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(xx_blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
}
+ (void)xx_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if(block) {
        block();
    }
}
@end


// TimerViewController.m
#import "TimerViewController.h"
#import "NSTimer+BlocksSupport.h"

@interface TimerViewController ()

@property (nonatomic,weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer =[NSTimer xx_scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{
            NSLog(@"it is log!");
    }];
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}

@end

低准确性

从 RunLoop 的机制图中可以看到CFRunLoopTimer 存在,CFRunLoopTimer 作为 RunLoop 的事件源之一,它的上层对应就是 NSTimer,NSTimer 的触发正是基于 RunLoop, 使用 NSTimer 之前必须注册到 RunLoop。一个NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 00:00, 00:02,00:04,00:06 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个 NSTimer,NSTimer 有个属性叫做 Tolerance 表示回调 NSTimer 的时间点容许多少最大误差。

如果 RunLoop 执行了一个很长时间的任务,错过了某个时间点,则那个时间点的回调也会跳过去,不会延后执行。比如 00:02 这个时间点被错过了,那么就只能等待下一个时间点 00:04 。因此如果对时间精度要求高的方法就不要使用NSTimer。

RunLoop模式

当调用scheduledTimerWithTimeInterval方法时,Timer会默认被加入到当前线程的RunLoop中,模式为NSDefaultRunLoopMode。如果当前线程是主线程(UI线程),比如UIScrollView的滚动操作,RunLoop模式自动会被切换成NSEventTrackingRunLoopMode,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件不会被执行,也就是说此时调用scheduledTimerWithTimeInterval添加到RunLoop中的Timer不会被执行。如果要让主线程中的Timer在页面滚动时也能被执行到,应使用NSRunLoopCommonModes

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 搜狗测试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档