iOS中的定时器

1,745 阅读7分钟

NSTimer

NSTimer是iOS中最常用的定时器。其通过Runloop来实现,一般情况下比较准确。但是当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoopRunLoopMode影响。

使用:

selector方式
/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

/// 构造但不开启
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

上面两个方法的使用:
//方法1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doTask) userInfo:nil repeats:YES];

//方法2
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

block方式
/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

/// 构造但不开启
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

上面两个方法的使用:
//方法1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"%s block ",__func__);
}];
    
//方法2
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"%s block ",__func__);
}];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode];

停止

//定时器的释放一定要先将其终止,而后才能销毁对象
- (void)invalidate;

//立即执行(fire)
//我们对定时器设置了延时之后,有时需要让它立刻执行,可以使用fire方法:
- (void)fire;

CADisplayLink

CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,每秒刷新60次。其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。

其使用步骤为 创建CADisplayLink->添加至RunLoop中->终止->销毁。代码如下

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

- (void)invalidate;

使用:
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTest)];
self.timer2.preferredFramesPerSecond = 1; //每秒1次
[self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

同时,由于其是基于屏幕刷新的,所以也度量单位是每帧,其提供了根据屏幕刷新来设置间隔的frameInterval属性,其决定于屏幕刷新多少帧时调用一次该方法,默认为1,即1/60秒调用一次。

在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于起进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。MBProcessHUD则是利用了这一特性.

存在的问题

使用NSTimerCADisplayLink,如果开发者不小心,可能会定时器无法销毁,导致内存泄漏问题。

实例代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode]; 
}

- (void)timerTest
{
    NSLog(@"%s ",__func__);
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"%s ",__func__);
}

会发现,当Controller关闭后,定时器还是持续打印,这就是定时器并没有及时的销毁。 出现这种情况得还有:

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
self.timer2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTest)];
self.timer2.preferredFramesPerSecond = 1;
[self.timer2 addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

这里要说明一下,采用block方式的定时器,会在Controller关闭后,自动销毁.

原因何在?

timer的target强引用self,同时self又强引用着timer,两者互相引用,造成了循环引用

解决方法:

第一种: 采用block方式

第二种: 自定义一个中间类充当target,并且这个类弱引用着self,即可

这个中间类就是NSProxy

NSProxy是一个抽象类,必须继承实例化其子类才能使用。

NSProxy效率较高,有方法就直接调用,若没有此方法会直接进入消息转发阶段没有缓存查找,消息发送,动态方法解析

什么是NSProxy:

  • NSProxy是一个抽象的基类,是根类,与NSObject类似
  • NSProxy和NSObject都实现了协议
  • 提供了消息转发的通用接口

下面我们就自定义类MyProxy继承与MyProxy

@interface MyProxy : MyProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation MyProxy
+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    MyProxy *proxy = [MyProxy alloc];
    proxy.target = target;
    return proxy;
}
//借用消息转发 使得原来类还能调用自己的sel
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

现在我们再使用selector方式创建定时器时:

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MyProxy proxyWithTarget:self] selector:@selector(doTask) userInfo:nil repeats:YES];

运行就会发现,当Controller关闭时,timer也会被销毁,不会造成循环引用。 同样,CADisplayLink也是这样。

当然开发中还有一种方式,也是借助中间类类解决循环引用问题,代码如下:

.h

@interface NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(**id**)aTarget
                                       selector:(**SEL**)aSelector
                                       userInfo:(**id**)userInfo
                                        repeats:(**BOOL**)repeats;
@end


.m

@interface TimerWeakObject : NSObject
///中间对象的弱引用指针
@property (nonatomic,weak) id target;
///定时器到时之后的一个回调方法
@property (nonatomic,assign) SEL selector;
///中间对象的弱引用指针
@property (nonatomic,weak) NSTimer *timer;

- (void)fire:(NSTimer *)timer;

@end

@implementation TimerWeakObject
/*对它所持有的target进行判断,若target存在,判断它是否响应选择器,
如果响应则执行对应的回调方法
否则就把timer置为无效,就可以达到Runloop对Timer强引用的释放,同时Timer也会对弱引用对象进行释放
*/
- (void)fire:(NSTimer *)timer
{
    if (self.target) {
        if ([self.target respondsToSelector:self.selector]) {
            [self.target performSelector:self.selector withObject:timer.userInfo];
        }
    }else{
        [self.timer invalidate];
    }
}

- (void)dealloc
{
    NSLog(@"TimerWeakObject dealloc...");
}

@end


@implementation NSTimer (WeakTimer)

/*
创建中间对象,把我们传进分类中的aTarget和aSelector指派给中间对象,
然后调用系统的NSTimer方法去创建NSTimer,
同时指定Timer的回调事件是中间对象的fire方法,
然后再fire方法中再对实际对象回调方法进行调用
*/
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats{
    TimerWeakObject *object = [[TimerWeakObject alloc] init];
    object.target = aTarget;
    object.selector = aSelector;
    object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
    return object.timer;
}

@end

使用定时器要这么复杂(借助其他类),难道就没有更简单一点了吗?

答案肯定是有的,那就是使用gcd的方式创建定时器,而且gcd的定时器不依赖与NSRunLoop,原理上来说会更加准确(在当前循环耗时操作较多时或者页面有点卡顿的时候)

gcd

GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理,通过系统级调用,更加精准

//创建定时器对象  gcd可以指定队列:即可以在主线程上也可以在子线程上完成任务
self.timer3 = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    
/// 参数: 1 定时器  2 任务开始时间   3任务的间隔  4可接受的误差时间,设置0即不允许出现误差
 dispatch_source_set_timer(self.timer3, DISPATCH_TIME_NOW, 1.0*NSEC_PER_SEC, 0.0*NSEC_PER_SEC);

//设置定时器任务
dispatch_source_set_event_handler(self.timer3, ^{
    NSLog(@"%s ",__func__);
});

// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(_gcdTimer);

gcd还有一种设置定时器任务的方式

dispatch_source_set_event_handler_f(self.timer3, gcdtimerTest);

不过设置这种方式,销毁的时候要在dealloc方法中调用:
dispatch_suspend(self.timer3);

封装定时器

这三种定时器中最准确的还是GCD,但是在使用的时候,会写很多代码,所以简单的封装一下。 (NSTimer受runloop影响,每圈的运行时间不确定,所以NSTimer不够精准)

关键点:

1、用一个字典存储每一个定时器,在取消的时候,根据定时器的key找到相应的定时器

2、多线程会造成线程不安全,对字典读写操作的时候需要加锁

@interface MBTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
           start:(NSTimeInterval)start
        interval:(NSTimeInterval)interval
         repeats:(BOOL)repeats
           async:(BOOL)async;

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async;

+ (void)cancelTask:(NSString *)name;

@end

@implementation MBTimer

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timers_ = [NSMutableDictionary dictionary];
        semaphore_ = dispatch_semaphore_create(1);
    });
}

+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
    
    // 队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置时间
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    // 定时器的唯一标识
    NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
    // 存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore_);
    
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();
        
        if (!repeats) { // 不重复的任务
            [self cancelTask:name];
        }
    });
    
    // 启动定时器
    dispatch_resume(timer);
    
    return name;
}

+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!target || !selector) return nil;
    
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [target performSelector:selector];
#pragma clang diagnostic pop
        }
    } start:start interval:interval repeats:repeats async:async];
}

+ (void)cancelTask:(NSString *)name
{
    if (name.length == 0) return;
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    
    dispatch_source_t timer = timers_[name];
    if (timer) {
        dispatch_source_cancel(timer);
        [timers_ removeObjectForKey:name];
    }

    dispatch_semaphore_signal(semaphore_);
}
@end

使用:

@property (copy, nonatomic) NSString *task;

//block方式:
//    self.task = [MBTimer execTask:^{
//        NSLog(@"%s ",__func__);
//    } start:0.5 interval:1.0 repeats:YES async:NO];
    
//selector方式
self.task = [MBTimer execTask:self selector:@selector(timerTest) start:0.5 interval:1.0 repeats:YES async:NO];
 
 
//取消任务
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [MBTimer cancelTask:self.task];
}

以上若有错误,欢迎指正。转载请注明出处。