iOS下的定时器之NSTimer & CADisplayLink & GCD

不打扰是莪最后的温柔 提交于 2020-08-16 02:53:55

概述

在日常的开发中,我们经常需要跟定时打交道,比如刷新界面动画,短信倒计时发送等,这便笔记总结了一些我在工作中使用到的一些开发,请大家多交流

NSTimer

iOS中最基础的定时器,本质是通过Runloop来实现的,一般情况下比较准确,但是当运行循环耗时操作比较多时,就会出现不准确。同时也受所加入Runloop的Mode影响,具体可以参考Runloop的特性

创建

构造方法主要分为自动启动和手动启动,手动启动的构造方法需要我们在创建NSTimer后启动它,iOS10之后还加入block的构造方法,防止内存泄漏,点赞

/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
// "scheduledTimer"前缀的为自动启动NSTimer的,如:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

/// 构造但不开启
// "timer"前缀的为只构造不启用的,如:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

/// iOS 10之后推出的手动启动构造方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
///iOS 10之后推出的自启动构造方法
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

这里需要说明下,定时器本质就是加入到了运行循环的Timer列表中,当前循环加入,没有特出意外情况,下个运行循环就会触发它,进而进行一系列操作。所以NSTimer除了构造还需要加入Runloop。

释放

定时器释放的时候一定将其中指,然后才能将其摧毁。

- (void)invalidate;

立即执行

我们对定时器设置了延时之后,有时需要立刻执行,可以使用fire方法:

- (void)fire;

CADisplayLink

CADisplayLink是基于屏幕刷新的周期,相比较NSTimer更精确,iOS屏幕的刷新频率位60次/秒,所以一次的时间是1/60s,如果你的间隔时间大于这个时间则建议你修改,否则会造成延迟。其本质也是通过Runloop,所以不难看出,当Runloop选择其他模式或者被耗时操作过多时,仍旧会造成延迟。

// 创建CADisplayLink
CADisplayLink *disLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkMethod)];
// 添加至RunLoop中
[disLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// 终止定时器
[disLink invalidate];
// 销毁对象
disLink = nil;

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

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);
		   // 取消 防止进一步调用
		   //dispatch_source_cancel(_gcdTimer);
    }
});
// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(_gcdTimer);

GCD更准确原因

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

定时器不准确的问题及解决

通过上文的叙述,我们大致了解不准确的原因,总结下主要是

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

知道了原因那么,很容易也就给出解决方案:

  • 避免过多耗时操作并发
  • 采用GCD定时器
  • 创建新的线程并开启Runloop,将定时器加入其中并将定时器加入的模式设置为NSRunLoopCommonModes

定时器内存泄漏问题

定时器在使用时应格外注意内存管理,常见情况时定时器对象无法释放造成内存泄漏,而严重的情况也会导致控制器无法释放,相关问题有如下:

NSTimer无法释放

NSTimer作为控制器的属性,被控制器强引用,创建NSTimer对象时控制器作为target被NSTimer强引用,即循环引用。

解决方案:

__weak typeof(self)weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    [weakSelf test];
}];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes];

NSTimer使用weak修饰无效果的原因

向target中传入weakSelf会失败,因为这个是对block才有效,因为block特性是如果外部变量使用的是弱指针进行引用,则block会对该变量有一个弱引用,同理,若是强指针进行引用,则block会对该变量有一个强引用.故传入弱指针解决循环引用只对block有效,定时器中传入的target,由于外部只是将参数地址传入,后赋值给timer内部对应的成员变量,故传入的是强指针还是弱指针是没有效果的。

简而言之:因为无论是weak还是strong修饰,在NSTimer中都会重新生成一个新的强引用指针指向self,导致循环引用的。

CADisplayLink无法释放

问题和解决思路跟NSTimer一致的,所以我就直接po解决方法

///中介.h文件
+ (instancetype)proxyWithTarget:(id)aTarget;

@property (nonatomic, weak) id target;

///中介.m文件
+ (instancetype)proxyWithTarget:(id)aTarget {
    FOSProxy *proxy = [FOSProxy alloc];
    proxy.target = aTarget;
    NSLog(@"0--%s",__func__);
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    
    NSLog(@"1--%s",__func__);
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    NSLog(@"2--%s",__func__);
    [invocation invokeWithTarget:self.target];
}

///VC调用.m
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    NSLog(@"---%s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor greenColor];
    [self test_proxy];
}

- (void)test_proxy {
    
    self.link = [CADisplayLink displayLinkWithTarget:[FOSProxy proxyWithTarget:self] selector:@selector(linkTest)];
    
    
     // 内存泄漏
//    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
}


- (void)linkTest {
    
    NSLog(@"sssss");
}

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


@end

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!