iOS中的三种定时器


NSTimer

一、背景

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

二、使用

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

@implementation NSTimer (weakTarget)

+ (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer * _Nonnull))block
{
return [self scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:self selector:@selector(timerEvent:) userInfo:block repeats:repeats];
} + (void)timerEvent:(NSTimer *)timer
{
void (^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
} @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:方法来处理自己没有实现的消息。

- (void)forwardInvocation:(NSInvocation *)invocation;
- (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中实现。

#import <Foundation/Foundation.h>

@interface YLWeakselfProxy : NSProxy

@property (nonatomic, weak) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end
#import "YLWeakselfProxy.h"

@implementation YLWeakselfProxy

+ (instancetype)proxyWithTarget:(id)target
{
YLWeakselfProxy *proxy = [YLWeakselfProxy alloc]; proxy.target = target; return proxy;
} - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{ if (self.target && [self.target respondsToSelector:sel]) {
return [self.target methodSignatureForSelector:sel];
}
return [super methodSignatureForSelector:sel];
} - (void)forwardInvocation:(NSInvocation *)invocation
{
SEL sel = [invocation selector]; if (self.target && [self.target respondsToSelector:sel]) {
[invocation invokeWithTarget:self.target];
}else{
[super forwardInvocation:invocation];
}
}
@end
@interface NSTimerViewController ()

@property (nonatomic, weak) NSTimer *timer;

@end

@implementation NSTimerViewController

- (void)viewDidLoad {
[super viewDidLoad]; self.title = @"NSTimerViewController"; self.view.backgroundColor = [UIColor redColor]; //方法一 (中间代理对象)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[YLWeakselfProxy proxyWithTarget:self] selector:@selector(runTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
self.timer = timer;
} - (void)runTimer
{
NSLog(@"=======%s",__func__);
} - (void)dealloc
{
[self.timer invalidate]; NSLog(@"=======%s",__func__);
}
@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的使用

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

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

[self.displayLink invalidate];
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是系统级别的源事件,精度很高,系统自动触发。

/**
* 创建DispatchSourceTimer对象
* flags: 一个数组,(暂时不知干吗用的)
* queue: timer 在那个队列里面执
*/
public class func makeTimerSource(flags: DispatchSource.TimerFlags = [], queue: DispatchQueue? = nil) -> DispatchSourceTimer
/**
* 单次执行
* deadline: 什么时候开始
*/
public func scheduleOneshot(deadline: DispatchTime, leeway: DispatchTimeInterval = .nanoseconds(0)) /**
* 重复执行
* deadline: 什么时候开始
* repeating: 调用频率,即多久调用一次
* leeway: 误差时间(微秒)
*/
public func schedule(deadline: DispatchTime, repeating interval: Double, leeway: DispatchTimeInterval = .nanoseconds(0)) /**
* 事件回调
* handler: 回调事件
*/
public func setEventHandler(handler: DispatchWorkItem)
import UIKit

class YLTimerTool: NSObject {

    private var timer: DispatchSourceTimer?
override init() {
super.init()
} deinit {
timer?.cancel()
timer = nil
} func gcdDispatchTime(intervel: TimeInterval, eventHandleClosure:@escaping (() -> Void)){ if timer == nil {
timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
//2. 默认主线程
// let timer = DispatchSource.makeTimerSource()
timer?.schedule(deadline: DispatchTime.now(), repeating: intervel, leeway: .milliseconds(10))
timer?.setEventHandler(handler: {
DispatchQueue.main.async {
eventHandleClosure()
}
})
self.resume()
}else{
timer?.setEventHandler(handler: {
DispatchQueue.main.async {
eventHandleClosure()
}
})
}
}
// 销毁
func invalidate() {
timer?.cancel()
timer = nil
}
// 挂起()
func stop() {
timer?.suspend()
}
// 启动
func resume() {
timer?.resume()
}
}

特性

  • 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. [深度学习] RBM及DBN

    转载于:http://blog.csdn.net/app_12062011/article/details/54313082 我们目前的讨论的神经网络,虽然学习算法不同,但基本上架构还是相同的,就是都 ...

  2. 时钟同步服务器ntp安装文档

    应用场景 同步时钟很有必要,如果服务器的时间差过大会出现不必要的问题 大数据产生与处理系统是各种计算设备集群的,计算设备将统一.同步的标准时间用于记录各种事件发生时序, 如E-MAIL信息.文件创建和 ...

  3. Redis缓存何以一枝独秀?——从百变应用场景与热门面试题中感受下Redis的核心特性与使用注意点

    大家好,又见面了. 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面.如果感兴趣,欢迎关注以获取后续更新. 作为<深入理解缓存原理与实战设计 ...

  4. 万字干货! 使用docker部署jenkins和gitlab

    阅读本文, 需要有基础的Git, Linux, Docker, Java, Maven, shell知识, 并最少有一台内存16G以上并已经安装好了Docker的机器. 1. 概述 2. 容器互联 3 ...

  5. [数据结构]克鲁斯卡尔(Kruskal)算法

    算法的概念 与Prim算法从顶点开始扩展最小生成树不同,Kruskal算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法.假设N=(V,E)是连通网,对应的最小生成树T=(Vt,Et),Kr ...

  6. win32com操作word 第二集:Application&Documents接口

    本课程<win32com操作word API精讲&项目实战>以视频为主,文字教程为辅,公众号ID:一灯编程. 先回答一个网友私信问题: win32com和微软的word接口文档有什 ...

  7. Array.from() ------来自❀ 前端宇宙 ❀公众号。

    JavaScript 中有一个这样的函数: Array.from:允许在 JavaScript 集合(如: 数组.类数组对象.或者是字符串.map .set 等可迭代对象) 上进行有用的转换. 1. ...

  8. BUG日记--——Linux安装Docker

    1.yum makecache fast不适合Centos8 2.解决办法 3.doucke的使用 1.关闭防火墙 # 关闭 systemctl stop firewalld # 禁止开机启动防火墙 ...

  9. 【新晋开源项目】内网穿透神器[中微子代理] 加入 Dromara 开源社区

    1.关于作者 dromara开源组织成员,dromara/neutrino-proxy项目作者 名称:傲世孤尘.雨韵诗泽 名言: 扎根土壤,心向太阳.积蓄能量,绽放微光. 拘浊酒邀明月,借赤日暖苍穹. ...

  10. 财务精度:BigInteger 与 BigDecimal

    财务精度:BigInteger 与 BigDecimal 每博一文案 师父说: 人这一辈子,真地好难. 有些人,好着好着,忽然就变陌生了,有些手,牵着牵着,瞬间就放开了,有些路,走着走着,就失去了方向 ...