iOS的几种定时器及区别

2018-11-27  本文已影响11人  羽裳有涯

在软件开发过程中,我们常常需要在某个时间后执行某个方法,或者是按照某个周期一直执行某个方法。在这个时候,我们就需要用到定时器。

然而,在iOS中有很多方法完成以上的任务,到底有多少种方法呢?经过查阅资料,大概有三种方法:NSTimer、CADisplayLink、GCD。接下来我就一一介绍它们的用法。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayersetNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是toll-free bridged的。一个 NSTimer 注册到RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和NSTimer并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,

一、NSTimer

  1. 创建方法

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];
TimerInterval : 执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法
target : 需要执行方法的对象。
selector : 需要执行的方法
repeats : 是否需要循环

  1. 释放方法

[timer invalidate];
注意 :
调用创建方法后,target对象的计数器会加1,直到执行完毕,自动减1。如果是 循环执行的话,就必须手动关闭,否则可以不执行释放方法。

  1. 特性

存在延迟
不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。

必须加入Runloop
使用上面的创建方式,会自动把timer加入MainRunloop的NSDefaultRunLoopMode中。如果使用以下方式创建定时器,就必须手动加入Runloop:

NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

二、CADisplayLink

  1. 创建方法
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];    
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  1. 停止方法
[self.displayLink invalidate];  
self.displayLink = nil;

当把CADisplayLink对象add到runloop中后,selector就能被周期性调用,类似于重复的NSTimer被启动了;执行invalidate操作时,CADisplayLink对象就会从runloop中移除,selector调用也随即停止,类似于NSTimer的invalidate方法。

  1. 特性

屏幕刷新时调用
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率60次/秒
延迟
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。
如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
使用场景
从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。

  1. 重要属性

frameInterval
NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。
duration
readOnly的CFTimeInterval值,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval。

三、GCD方式

GCD定时器是dispatch_source_t类型的变量,其可以实现更加精准的定时效果。我们来看看如何使用:

/** 创建定时器对象
 * para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
 * para2-3: 中间两个参数对定时器无用
 * para4: 最后为在什么调度队列中使用
 */
_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
/** 设置定时器
 * para2: 任务开始时间
 * para3: 任务的间隔
 * para4: 可接受的误差时间,设置0即不允许出现误差
 * Tips: 单位均为纳秒
 */
dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
/** 设置定时器任务
 * 可以通过block方式
 * 也可以通过C函数方式
 */
dispatch_source_set_event_handler(_gcdTimer, ^{
    static int gcdIdx = 0;
    NSLog(@"GCD Method: %d", gcdIdx++);
    NSLog(@"%@", [NSThread currentThread]);

    if(gcdIdx == 5) {
        // 终止定时器
        dispatch_suspend(_gcdTimer);
    }
});
// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(_gcdTimer);

#import "NSObject+NNBTimerManager.h"
#import "NNBTimerManager.h"

#define Manager  [NNBTimerManager sharedInstance]

@implementation NSObject (NNBTimerManager)

- (void)nnbCreateTimerInterval:(double)interval
                      callback:(NNBTimerBlockCallBack)callback {
    
    [Manager scheduleTimerWithName:[NSString stringWithFormat:@"%lld", (long long)self]
                             interval:interval
                              block:callback];
}

- (void)nnbDeleteTimer {
    [Manager deleteTimerWithName:[NSString stringWithFormat:@"%lld", (long long)self]];
}

- (void)nnbDeleteAllTimers {
    [Manager deleteAllTimer];
}

@end
typedef void (^NNBTimerBlockCallBack)(void);

@interface NNBTimerManager : NSObject

// 单例
+ (NNBTimerManager *)sharedInstance;

/**
 启动一个timer
 @param timerName       timer的名称,作为唯一标识。
 @param interval        执行的时间间隔。
 @param callback          时间间隔到点时执行的block。
 */

- (void)scheduleTimerWithName:(NSString *)timerName
                     interval:(double)interval
                        block:(NNBTimerBlockCallBack)callback;
/**
 取消timer
 @param timerName timer的名称,作为唯一标识。
 */
- (void)deleteTimerWithName:(NSString *)timerName;

/**
 * 取消所有timer
 */
- (void)deleteAllTimer;
@end
@interface NNBTimerManager ()
@property (nonatomic, strong) NSMutableDictionary *timerArry;
@property (nonatomic, strong) NSMutableDictionary *timerActionBlockCacheArry;

@end

@implementation NNBTimerManager

#pragma mark - Public Method

// 单例
+ (NNBTimerManager *)sharedInstance {
    
    static NNBTimerManager *_NNBTimerManager = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _NNBTimerManager = [[NNBTimerManager alloc] init];
    });
    
    return _NNBTimerManager;
}

- (void)scheduleTimerWithName:(NSString *)timerName
                     interval:(double)interval
                        block:(NNBTimerBlockCallBack)callback {
    MLOG(@"创建时定时器指针名字:%@", timerName);

    // timer将被放入的队列queue
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 创建dispatch_source_t的timer
    dispatch_source_t timer = [self.timerArry objectForKey:timerName];
    
    if (nil == timer) {
        timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        //启动timer
        dispatch_resume(timer);
        [self.timerArry setObject:timer forKey:timerName];
    }
    
    /**
     * dispatch_source_t:为source源
     * dispatch_time_t start:定时器开始时间,delat为增量(当前时间基础上,增加X秒为开始计时时间)
     * interval:定时时长
     * leeway:允许误差,
     */
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, 0),
                              interval * NSEC_PER_SEC,
                              0.1 * NSEC_PER_SEC);
    
    // 移除上一次任务
    [self removeActionCacheForTimer:timerName];
    
    // 时间间隔到点时执行block
    dispatch_source_set_event_handler(timer, ^{
        callback();
    });
}

// 取消timer
- (void)deleteTimerWithName:(NSString *)timerName {
    MLOG(@"取消时定时器指针名字:%@", timerName);
    
    dispatch_source_t timer = [self.timerArry objectForKey:timerName];
    
    if (!timer) {
        return;
    }
    
    [self.timerArry removeObjectForKey:timerName];
    [self.timerActionBlockCacheArry removeObjectForKey:timerName];
    dispatch_source_cancel(timer);
}

//取消所有timer
- (void)deleteAllTimer {
    mzWeakSelf(weakSelf);
    [self.timerArry enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        [weakSelf deleteTimerWithName:key];
    }];
}

#pragma mark - getter & setter

- (NSMutableDictionary *)timerArry {
    if (!_timerArry) {
        _timerArry = [[NSMutableDictionary alloc] init];
    }
    return _timerArry;
}

- (NSMutableDictionary *)timerActionBlockCacheArry {
    if (!_timerActionBlockCacheArry) {
        _timerActionBlockCacheArry = [[NSMutableDictionary alloc] init];
    }
    return _timerActionBlockCacheArry;
}

#pragma mark - private method

- (void)cacheAction:(dispatch_block_t)action forTimer:(NSString *)timerName {
    id actionArray = [self.timerActionBlockCacheArry objectForKey:timerName];
    
    if (actionArray && [actionArray isKindOfClass:[NSMutableArray class]]) {
        [(NSMutableArray *)actionArray addObject:action];
    }else {
        NSMutableArray *array = [NSMutableArray arrayWithObject:action];
        [self.timerActionBlockCacheArry setObject:array forKey:timerName];
    }
}

- (void)removeActionCacheForTimer:(NSString *)timerName {
    if (![self.timerActionBlockCacheArry objectForKey:timerName]) {
        return;
    }
    [self.timerActionBlockCacheArry removeObjectForKey:timerName];
}
@end

GCD更准时的原因

通过观察代码,我们可以发现GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理。dispatch类似生产者消费者模式,通过监听系统内核对象,在生产者生产数据后自动通知相应的dispatch队列执行,后者充当消费者。通过系统级调用,更加精准。

定时器不准时的问题及解决
通过上文的叙述,我们大致了解了定时器不准时的原因,总结一下主要是

  • 当前RunLoop过于繁忙
  • RunLoop模式与定时器所在模式不同

上面解释了GCD更加准时的原因,所以解决方案也不难得出:

  • 避免过多耗时操作并发
  • 采用GCD定时器
  • 创建新线程并开启RunLoop,将定时器加入其中(适度使用)
  • 将定时器添加到NSRunLoopCommonModes(使用不当会阻塞UI响应)

其中后两者在使用前应确保合理使用,否则会产生负面影响。

定时器的内存泄露问题
定时器在使用时应格外注意内存管理,常见情况时定时器对象无法释放造成内存泄露,而严重情况会造成控制器也无法释放,危害更大。其内存泄露有两部分问题,我们先来看第一部分:

问题1: NSTimer无法释放
我们知道,NSTimer实际上是加入到RunLoop中的,那么在其启动时其被RunLoop强引用,那么即使我们在后面将定时器设为nil,也只是引用计数减少了1,其仍因为被RunLoop引用而无法释放,造成内存泄露。

问题2: 控制器无法释放
这是NSTimer无法释放所造成的更严重问题,由于为定时器设置了target,控制器就会得到一个来自定时器的引用。我们来分析一下这个情况,首先定时器必须被强引用,否则将在autoreleasepool之后被释放掉造成野指针。而定时器的target又对控制器有一个强引用,这就是典型的强引用循环(循环引用)。

那么如何解决这两个问题呢?答案就是使用invalidate方法。

苹果文档介绍如下:
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

即,invalidate方法会将定时器从RunLoop中移除,同时解除对target等对象的强引用。

CADisplayLink同理,而GCD定时器则使用dispatch_suspend()

上一篇下一篇

猜你喜欢

热点阅读