iOS中的三种定时器
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中的三种定时器的更多相关文章
- iOS中的几种定时器详解
在软件开发过程中,我们常常需要在某个时间后执行某个方法,或者是按照某个周期一直执行某个方法.在这个时候,我们就需要用到定时器. 然而,在iOS中有很多方法完成以上的任务,经过查阅资料,大概有三种方法: ...
- ios中的三种弹框《转》
目前为止,已经知道3种IOS弹框: 1.系统弹框-底部弹框 UIActionSheet (1)用法:处理用户非常危险的操作,比如注销系统等 (2)举例: UIActionSheet *sheet = ...
- ios中的三种弹框
目前为止,已经知道3种IOS弹框: 1.系统弹框-底部弹框 UIActionSheet (1)用法:处理用户非常危险的操作,比如注销系统等 (2)举例: UIActionSheet *sheet = ...
- Objective-C三种定时器CADisplayLink / NSTimer / GCD的使用
OC中的三种定时器:CADisplayLink.NSTimer.GCD 我们先来看看CADiskplayLink, 点进头文件里面看看, 用注释来说明下 @interface CADisplayLin ...
- C#中三种定时器对象的比较 【转】
https://www.cnblogs.com/zxtceq/p/5667281.html C#中三种定时器对象的比较 ·关于C#中timer类 在C#里关于定时器类就有3个1.定义在System.W ...
- cocos2dx三种定时器使用
cocos2dx三种定时器的使用以及停止schedule.scheduleUpdate.scheduleOnce 今天白白跟大家分享一下cocos2dx中定时器的用法. 首先,什么是定时 ...
- IOS开发数据存储篇—IOS中的几种数据存储方式
IOS开发数据存储篇—IOS中的几种数据存储方式 发表于2016/4/5 21:02:09 421人阅读 分类: 数据存储 在项目开发当中,我们经常会对一些数据进行本地缓存处理.离线缓存的数据一般都 ...
- Java三大框架之——Hibernate中的三种数据持久状态和缓存机制
Hibernate中的三种状态 瞬时状态:刚创建的对象还没有被Session持久化.缓存中不存在这个对象的数据并且数据库中没有这个对象对应的数据为瞬时状态这个时候是没有OID. 持久状态:对象经过 ...
- Asp.Net中的三种分页方式
Asp.Net中的三种分页方式 通常分页有3种方法,分别是asp.net自带的数据显示空间如GridView等自带的分页,第三方分页控件如aspnetpager,存储过程分页等. 第一种:使用Grid ...
- httpClient中的三种超时设置小结
httpClient中的三种超时设置小结 本文章给大家介绍一下关于Java中httpClient中的三种超时设置小结,希望此教程能给各位朋友带来帮助. ConnectTimeoutExceptio ...
随机推荐
- SSM框架——SpringMVC
SpringMVC MVC三层架构 Controller层:取得前端数据.调用相关业务逻辑.转发/重定向到其他页面 Model层:实现业务逻辑.保存数据 View层:显示页面 1.第一个MVC程序 新 ...
- Pytorch:单卡多进程并行训练
1 导引 我们在博客<Python:多进程并行编程与进程池>中介绍了如何使用Python的multiprocessing模块进行并行编程.不过在深度学习的项目中,我们进行单机多进程编程时一 ...
- 不止ChatGPT,谷歌云 AI 方案早已厉兵秣马!
近日 ChatGPT 爆火,掀起热议,能聊天能写代码,还能写策划稿,AI 似乎已逐渐变得无所不能. 不过在 AI 对话上,谷歌早在17年就提出了 Dialogflow 这一AI对话平台.相比起 Cha ...
- 1.31 wlx 魔怔 9 解法交互题小结
参考题解地址 1. 从树上任意一个节点开始,跳到其随机一个后代,跳到叶子的期望次数为 \(H_{siz_u}=\ln(siz_u)\). 证明: 首先考虑一条链的情况.设在第 \(i\) 个点期望次数 ...
- Linux服务器硬件及RAID配置
Linux服务器硬件及RAID配置 一.RAID磁盘阵列介绍 独立冗余磁盘阵列(Redundant Array of Independent Disks) 作用: 把多块独立的物理硬盘按不同的方式组合 ...
- 安装Windows Server 2022 - 初学者系列 - 学习者系列文章
这天要写一个关于系统部署的系列文章,涉及到Windows Server 2022操作系统的安装,所以就写了此文.Windows系列的操作系统安装,以前的博文中都有介绍,这里再次做一个安装描述吧.需要的 ...
- Pycharm中图标的含义
Pycharm中图标的含义 问题 有同学问,下面的v,c,f等都是啥意思 这个问题嘛,应该在python学习阶段来问,不过我也只能解释部分,有些也只能靠猜测 按图索骥找了下pycharm的官网doc, ...
- drf-认证、权限、频率、过滤、排序、分页
1.认证组件 1.1 局部认证 1.首先写两个接口,一个查询单个一个查询所有,我们利用视图扩展类和视图子类写在一个视图类上: views.py: from rest_framework.viewset ...
- 和ChatGPT聊了一会天它的学习反映能力惊呆了我
我:用java写一段玫瑰花 ChatGPT:您可以使用 Java 绘制玫瑰花.以下是一段代码: import java.awt.*; import javax.swing.*; public clas ...
- python 取整方法
1.向下取整: int() 2.向上取整:ceil() 使用ceil()方法时需要导入math模块,例如 3.四舍五入:round() 4.分别取 将整数部分和小数部分分别取出,可以使用math模块中 ...