iOS中的三种定时器


NSTimer

一、背景

定时器是iOS开发中经常使用的,但是使用不慎会造成内存泄露,因为NSTimer没有释放,控制器析构函数dealloc也没有调用,造成内存泄露。

二、使用

  1. swift
  2. //MARK: swift语言中是没有NSInvocation类,可以使用 OC 的方法做桥接处理
  3. open class func scheduledTimer(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool) -> Timer
  4. //MARK: 实例方法创建的定时器需要使用 fire 来启动定时器,否则,该定时器不起作用。而且需要手动添加到runloop(RunLoop.current.add(_ timer: Timer, forMode mode: RunLoop.Mode))
  5. @available(iOS 10.0, *)
  6. public /*not inherited*/ init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
  7. public init(fireAt date: Date, interval ti: TimeInterval, target t: Any, selector s: Selector, userInfo ui: Any?, repeats rep: Bool)
  8. //MARK: 类方法(静态方法)创建的定时器方法,自动开启定时器,自动加入runloop
  9. @available(iOS 10.0, *)
  10. open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer
  11. open class func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer

二、使用要点

1.定时器与runloop

官方文档描述:

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

翻译:计时器与runlopp一起工作。Runloop维护对计时器的强引用,因此在将计时器添加到Runloop后,您不必维护自己对计时器的强引用。

-invalidate的作用

由于runloop对timer强引用,runloop如何释放timer呢?-invalidate函数就是释放timer的,来看看官方文档描述:

Stops the timer from ever firing again and requests its removal from its run loop.

据官方介绍可知,- invalidate做了两件事,首先是把本身(定时器)从NSRunLoop中移除,然后就是释放对‘target’对象的强引用。从而解决定时器带来的内存泄露问题。

内存泄露在哪?

先上一个图(为了方便讲解,途中箭头指向谁就代表强引谁)

如果创建定时器只是简单的计时,不做其他引用,那么timer对象与ViewController对象循环引用的问题就可以避免,即图中 箭头4可避免。

但是如果在定时器里做了和UIViewController相关的事情,就存在内存泄露问题,因为UIViewController引用timer,timer强引用target(就是UIViewController),同时timer直接被NSRunLoop强引用着,从而导致内存泄露。

有些人可能会说对timer对象发送一个invalidate消息,这样NSRunLoop即不会对timer进行强引,同时timer也会释放对target对象的强引,这样不就解决了吗?没错,内存泄露是解决了。

但是,这并不是我们想要的结果,在开发中我们可能会遇到某些需求,只有在UIViweController对象要被释放时才去释放timer(此处要注意释放的先后顺序及释放条件),如果提前向timer发送了invalidate消息,那么UIViweController对象可能会因为timer被提前释放而导致数据错了,就像闹钟失去了秒针一样,就无法正常工作了。所以我们要做的是在向UIViweController对象发送dealloc消息前在给timer发送invalidate消息,从而避免本末倒置的问题。这种情况就像一个死循环(因为如果不给timer发送invalidate消息,UIViweController对象根本不会被销毁,dealloc方法根本不会执行),那么该怎么做呢?

如何解决?

现在我们已经知道内存泄露在哪了,也知道原因是什么,那么如何解决,或者说怎样优雅的解决这问题呢?方式有很多.

  • NSTimer Target

将定时器中的‘target’对象替换成定时器自己,采用分类实现。

  1. @implementation NSTimer (weakTarget)
  2. + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer * _Nonnull))block
  3. {
  4. return [self scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:self selector:@selector(timerEvent:) userInfo:block repeats:repeats];
  5. }
  6. + (void)timerEvent:(NSTimer *)timer
  7. {
  8. void (^block)(NSTimer *timer) = timer.userInfo;
  9. if (block) {
  10. block(timer);
  11. }
  12. }
  13. @end
  • NSProxy:NSProxy

NSProxy implements the basic methods required of a root class, including those defined in the NSObjectProtocol protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation(_ and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself

NSProxy 是一个抽象类,它接收到任何自己没有定义的方法他都会产生一个异常,所以一个实际的子类必须提供一个初始化方法或者创建方法,并且重载forwardInvocation:方法和methodSignatureForSelector:方法来处理自己没有实现的消息。

  1. - (void)forwardInvocation:(NSInvocation *)invocation;
  2. - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");

从类名来看是代理类,专门负责代理对象转发消息的。相比NSObject类来说NSProxy更轻量级,通过NSProxy可以帮助Objective-C间接的实现多重继承的功能。

解决方案:利用消息转发来断开NSTimer对象与视图之间的引用关系。初始化NSTimer时把触发事件的target替换成一个单独的对象,然后这个对象中NSTimer的SEL方法触发时让这个方法在当前的视图self中实现。

  1. #import <Foundation/Foundation.h>
  2. @interface YLWeakselfProxy : NSProxy
  3. @property (nonatomic, weak) id target;
  4. + (instancetype)proxyWithTarget:(id)target;
  5. @end
  1. #import "YLWeakselfProxy.h"
  2. @implementation YLWeakselfProxy
  3. + (instancetype)proxyWithTarget:(id)target
  4. {
  5. YLWeakselfProxy *proxy = [YLWeakselfProxy alloc];
  6. proxy.target = target;
  7. return proxy;
  8. }
  9. - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
  10. {
  11. if (self.target && [self.target respondsToSelector:sel]) {
  12. return [self.target methodSignatureForSelector:sel];
  13. }
  14. return [super methodSignatureForSelector:sel];
  15. }
  16. - (void)forwardInvocation:(NSInvocation *)invocation
  17. {
  18. SEL sel = [invocation selector];
  19. if (self.target && [self.target respondsToSelector:sel]) {
  20. [invocation invokeWithTarget:self.target];
  21. }else{
  22. [super forwardInvocation:invocation];
  23. }
  24. }
  25. @end
  1. @interface NSTimerViewController ()
  2. @property (nonatomic, weak) NSTimer *timer;
  3. @end
  4. @implementation NSTimerViewController
  5. - (void)viewDidLoad {
  6. [super viewDidLoad];
  7. self.title = @"NSTimerViewController";
  8. self.view.backgroundColor = [UIColor redColor];
  9. //方法一 (中间代理对象)
  10. NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[YLWeakselfProxy proxyWithTarget:self] selector:@selector(runTimer) userInfo:nil repeats:YES];
  11. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  12. self.timer = timer;
  13. }
  14. - (void)runTimer
  15. {
  16. NSLog(@"=======%s",__func__);
  17. }
  18. - (void)dealloc
  19. {
  20. [self.timer invalidate];
  21. NSLog(@"=======%s",__func__);
  22. }
  23. @end
  • Block法

思路就是使用block的形式替换掉原先的“target-selector”方式,打断_timer对于其他对象的引用,

官方已经在iOS10之后加入了新的api,从而支持了block形式创建timer:

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.

/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead

/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.

/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references

+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

根据翻译,加入block形式就是为了避免引用循环。简单来说就是使用userInfo这个参数去传递block给selector去进行执行,target是timer自己,不会造成引用循环。还有一个需要注意的地方就是规避block的引用循环,为什么之类的详细解释不在这说了。

特性

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

CADisplayLink

文档官方:

A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

CADisplayLink其实就是一个定时器对象,是一个能让我们以和屏幕刷新率(60HZ)同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink跟CoreAnimation类都属于QunartzCore.framework中API。

CADisplayLink的使用

  1. self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerRunEvent)];
  2. self.displayLink.frameInterval = 60;
  3. [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

创建CADisplayLink并将其添加到Runloop中。这样timerRunEvent(@selector)就会被周期调用,其中使用frameInterval设置调用的间隔,上方代表每秒调用一次(因为屏幕的刷新频率为60HZ,即每秒60次)。

  1. [self.displayLink invalidate];
  2. self.displayLink = nil;

关于精度

相对于NSTimer,CADisplayLink的精度更加准确些,毕竟苹果设备的屏幕的刷新频率是固定的,都是60HZ。而CADisplayLink在每次刷新结束后都会被调用,精度会比较高。同时我们也可以根据CADisplayLink这个特性来检测屏幕是否会出现掉帧现象,如:<YYKit 中计算当前界面每秒 FPS 帧数的小组件>

就是根据此种原理。

关于使用场景

CADisplayLink的使用场景比较单一,适用于UI、动画的绘制与渲染。而比较著名的Facebook开源的第三方库POP就是基于CADisplayLink的,性能上比系统的CoreAnimation更加优秀。

而NSTimer在使用上就会更加的广泛了,基本很多场景都可使用。不管是一次性的还是连续周期性的timer事件,都会将NSTimer对象添加到Runloop中,但当Runloop正在执行另一个任务时,timer就会出现延迟。

特性

  • 屏幕刷新时调用CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
  • 延迟iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
  • tolerance属性用于设置可以容忍的触发时间的延迟范围。
  • 同样注意内存泄露问题,原理和NSTimer一样。

CGD定时器

A dispatch source that submits the event handler block based on a timer.

大概意思是分派源基于计时器提交事件处理程序块。dispatch_source_t的定时器不受RunLoop影响,而且dispatch_source_t是系统级别的源事件,精度很高,系统自动触发。

  1. /**
  2. * 创建DispatchSourceTimer对象
  3. * flags: 一个数组,(暂时不知干吗用的)
  4. * queue: timer 在那个队列里面执
  5. */
  6. public class func makeTimerSource(flags: DispatchSource.TimerFlags = [], queue: DispatchQueue? = nil) -> DispatchSourceTimer
  7. /**
  8. * 单次执行
  9. * deadline: 什么时候开始
  10. */
  11. public func scheduleOneshot(deadline: DispatchTime, leeway: DispatchTimeInterval = .nanoseconds(0))
  12. /**
  13. * 重复执行
  14. * deadline: 什么时候开始
  15. * repeating: 调用频率,即多久调用一次
  16. * leeway: 误差时间(微秒)
  17. */
  18. public func schedule(deadline: DispatchTime, repeating interval: Double, leeway: DispatchTimeInterval = .nanoseconds(0))
  19. /**
  20. * 事件回调
  21. * handler: 回调事件
  22. */
  23. public func setEventHandler(handler: DispatchWorkItem)
  1. import UIKit
  2. class YLTimerTool: NSObject {
  3. private var timer: DispatchSourceTimer?
  4. override init() {
  5. super.init()
  6. }
  7. deinit {
  8. timer?.cancel()
  9. timer = nil
  10. }
  11. func gcdDispatchTime(intervel: TimeInterval, eventHandleClosure:@escaping (() -> Void)){
  12. if timer == nil {
  13. timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
  14. //2. 默认主线程
  15. // let timer = DispatchSource.makeTimerSource()
  16. timer?.schedule(deadline: DispatchTime.now(), repeating: intervel, leeway: .milliseconds(10))
  17. timer?.setEventHandler(handler: {
  18. DispatchQueue.main.async {
  19. eventHandleClosure()
  20. }
  21. })
  22. self.resume()
  23. }else{
  24. timer?.setEventHandler(handler: {
  25. DispatchQueue.main.async {
  26. eventHandleClosure()
  27. }
  28. })
  29. }
  30. }
  31. // 销毁
  32. func invalidate() {
  33. timer?.cancel()
  34. timer = nil
  35. }
  36. // 挂起()
  37. func stop() {
  38. timer?.suspend()
  39. }
  40. // 启动
  41. func resume() {
  42. timer?.resume()
  43. }
  44. }

特性

  • GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理。dispatch类似生产者消费者模式,通过监听系统内核对象,在生产者生产数据后自动通知相应的dispatch队列执行,后者充当消费者。通过系统级调用,更加精准。
  • 可以使用子线程,解决定时间跑在主线程上卡UI问题
  • 需要将dispatch_source_t timer设置为成员变量,不然会立即释放

参考

iOS中的三种定时器的更多相关文章

  1. iOS中的几种定时器详解

    在软件开发过程中,我们常常需要在某个时间后执行某个方法,或者是按照某个周期一直执行某个方法.在这个时候,我们就需要用到定时器. 然而,在iOS中有很多方法完成以上的任务,经过查阅资料,大概有三种方法: ...

  2. ios中的三种弹框《转》

    目前为止,已经知道3种IOS弹框: 1.系统弹框-底部弹框 UIActionSheet  (1)用法:处理用户非常危险的操作,比如注销系统等 (2)举例: UIActionSheet *sheet = ...

  3. ios中的三种弹框

    目前为止,已经知道3种IOS弹框: 1.系统弹框-底部弹框 UIActionSheet  (1)用法:处理用户非常危险的操作,比如注销系统等 (2)举例: UIActionSheet *sheet = ...

  4. Objective-C三种定时器CADisplayLink / NSTimer / GCD的使用

    OC中的三种定时器:CADisplayLink.NSTimer.GCD 我们先来看看CADiskplayLink, 点进头文件里面看看, 用注释来说明下 @interface CADisplayLin ...

  5. C#中三种定时器对象的比较 【转】

    https://www.cnblogs.com/zxtceq/p/5667281.html C#中三种定时器对象的比较 ·关于C#中timer类 在C#里关于定时器类就有3个1.定义在System.W ...

  6. cocos2dx三种定时器使用

         cocos2dx三种定时器的使用以及停止schedule.scheduleUpdate.scheduleOnce 今天白白跟大家分享一下cocos2dx中定时器的用法. 首先,什么是定时 ...

  7. IOS开发数据存储篇—IOS中的几种数据存储方式

    IOS开发数据存储篇—IOS中的几种数据存储方式 发表于2016/4/5 21:02:09  421人阅读 分类: 数据存储 在项目开发当中,我们经常会对一些数据进行本地缓存处理.离线缓存的数据一般都 ...

  8. Java三大框架之——Hibernate中的三种数据持久状态和缓存机制

    Hibernate中的三种状态   瞬时状态:刚创建的对象还没有被Session持久化.缓存中不存在这个对象的数据并且数据库中没有这个对象对应的数据为瞬时状态这个时候是没有OID. 持久状态:对象经过 ...

  9. Asp.Net中的三种分页方式

    Asp.Net中的三种分页方式 通常分页有3种方法,分别是asp.net自带的数据显示空间如GridView等自带的分页,第三方分页控件如aspnetpager,存储过程分页等. 第一种:使用Grid ...

  10. httpClient中的三种超时设置小结

    httpClient中的三种超时设置小结   本文章给大家介绍一下关于Java中httpClient中的三种超时设置小结,希望此教程能给各位朋友带来帮助. ConnectTimeoutExceptio ...

随机推荐

  1. [seaborn] seaborn学习笔记7-常用参数调整Adjustment of Common Parameters

    7 常用参数调整Adjustment of Common Parameters(代码下载) 主要讲述关于seaborn通用参数设置方法,该章节主要内容有: 主题设置 themes adjustment ...

  2. kafka详解(01) - 概述

    kafka详解(01) - 概述 定义:Kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域. 消息队列 MQ传统应用场景之异步处理 使用消 ...

  3. Dubbo架构设计与源码解析(二) 服务注册

    作者:黄金 一.Dubbo简介 Dubbo是一款典型的高扩展.高性能.高可用的RPC微服务框架,用于解决微服务架构下的服务治理与通信问题.其核心模块包含 [RPC通信] 和 [服务治理] ,其中服务治 ...

  4. dp 优化

    dp 优化 \(\text{By DaiRuiChen007}\) I. [ARC085D] - NRE \(\text{Link}\) 思路分析 将最终的第 \(i\) 对 \(a_i\) 和 \( ...

  5. 第一个shell

    首先进入linux系统,打开命令行,输入命令vi test.sh创建一个shell测试脚本,键入i切换vi编辑器为输入模式,输入以下文本内容,键入:wq保存退出即可.下面第一行的#!是告诉系统其后路径 ...

  6. effective-c 条款2理解与思考

    尽量使用const,enum,inline替换 #define 因为,#define 替换发生在预处理阶段,编译器对这个替换内容就缺少了类型检测,并且不利于错误信息的查看 编译器再声明数组时必须知道数 ...

  7. 学习python的编程语言

    前言 那么多编程语言,为什么学python 易于学习,是所有编程语言当中最容易学习的 没有最好的语言,只有最合适的语言 第一章 python基础 1. 课程整体介绍 课程整体介绍 python编程基础 ...

  8. MRS_Debug仿真相关问题汇总

    解决问题如下: Debug时,看不到外设寄存器选项 Debug时,更改变量显示类型 Debug时,断点异常 跳过所有断点 取消仿真前自动下载程序 Debug时仅擦除程序代码部分flash空间 保存De ...

  9. 《自定义工作流配置,springboot集成activiti,前端vue,完整版审批单据》

    前言 activiti工作流引擎项目,企业erp.oa.hr.crm等企事业办公系统轻松落地,一套完整并且实际运用在多套项目中的案例,满足日常业务流程审批需求. 一.项目形式 springboot+v ...

  10. spring cloud alibaba - Nacos 作为配置中心基础使用

    1.简要说明 Nacos提供了作为配置中心的功能,只需要在Nacos的控制台页面添加配置,然后在项目中配置相应的"路径"就好. 主要分为几个步骤: 在Nacos控制台添加配置 在项 ...