当前位置: 首页 > news >正文

iOS开发 - NSTimer极限使用

目录

1.什么是NSTimer?

2.NSTimer和RunLoop的关系

3.定时器释放的方式

4.NSTimer的时间准确吗?

5.NSTimer的衍变之路

6.NSTimer如何避免循环引用

1)在ViewController即将消失时销毁定时器

2)对NSTimer进行二次封装

3)iOS10之后的新API

4)iOS10之前的API自己改造成block

5)使用NSProxy增加一个中间层subTarget


1.什么是NSTimer?

NSTimer是一个定时器,是一个面向对象的定时器。在经过一定的时间间隔后触发,向目标对象发送指定的消息。其工作原理是将一个监听加入到系统的runloop中去,当系统runloop执行到timer条件的循环时,会调用timer一次,如果是一个重复的定时器,当timer回调函数runloop之后,timer会再一次的将自己加入到runloop中去继续监听下一次timer事件。

2.NSTimer和RunLoop的关系

前面已经说过,NSTimer的原理是将定时器中的事件添加到runloop中,以实现循环的,这是因为定时器默认处于runloop中的kCFRunLoopDefaultMode,主线程默认也处于此mode下,定时器这才具备了这样的能力。所以,没有runloop,NSTimer完全无法工作。

这里提出一个经典的案例:定时器默认无法在页面滚动时执行。

原因是滚动时,主线程runloop处于UITrackingRunLoopMode,这时候,定时器所处runloop依然处于kCFRunLoopDefaultMode,就导致定时器线程被阻塞,要解决这一个问题,我们就需要定时器无论是在kCFRunLoopDefaultMode还是UITrackingRunLoopMode下都可以正常工作,这时候就需要用到runloop中的伪模式kCFRunLoopCommonMode,这并不是一个真正的mode,而是一种多mode的处理方式。具体做法如下:

//创建定时器
_timer=[NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
//添加到循环中
NSRunLoop *runloop=[NSRunLoop currentRunLoop];
[runloop addTimer:_timer forMode:NSRunLoopCommonModes];

3.定时器释放的方式

[_timer invalidate]; 
_timer = nil;

二者缺一不可。

如果是在VC中创建的NSTimer,这种情况下,self和_timer相互强引用,VC的Delloc方法不会执行,所以定时器的销毁方法不能放在Delloc中,需要放在viewWillDIsappear中,原因我们放到最后说明。

4.NSTimer的时间准确吗?

不准确!NSTimer不是采用实时机制!

NSTimer的精确度略低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

话都说到这里了,那肯定会有一种相对准确的方法,是的,CADisplayLink,但是其使用场景相对单一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

CADisplayLink其原理更为复杂,因为内部操作的是一个source,CADisplayLink也并非百分百准确,当在两次屏幕刷新之间执行了一个长任务时,就会有一帧被跳过去,这一点倒是和NSTimer相似。

还有另一种定时器DispatchSourceTimer,这里不再赘述,它的准确度也是要高于NSTimer的,适用于对精确度要求相对较高的场景。如果做秒杀的计时器,推荐这种方式来做。

5.NSTimer的衍变之路

iOS10以前:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;

iOS10以后:

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

+ (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));

- (instancetype)initWithFireDate:(NSDate *)date interval:(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的回调方法,block的作用就是将自身作为参数传递给block,来帮助避免循环引用,且使用起来更便捷,但要注意__weak和__strong的使用。

虽然如此,但还是有相当一部分人喜欢iOS10之前的API。

6.NSTimer如何避免循环引用

其实我们在前面也稍稍讲过一些,总结下来有如下几种方法:

  • 在ViewController即将消失时销毁定时器
  • 对NSTimer进行二次封装
  • iOS10之后的新API
  • iOS10之前的API自己改造成block
  • 使用NSProxy增加一个中间层subTarget

1)在ViewController即将消失时销毁定时器

[_timer invalidate]; 
_timer = nil;

由于VC对_timer的强引用导致VC在销毁时Delloc方法无法执行,所以需要将销毁方法移步ViewWillDIsappear执行。

2)对NSTimer进行二次封装

#import <Foundation/Foundation.h>
@interface LHTimer : NSObject

//创建定时器
- (void)startTimer;

//销毁定时器
- (void)destroyTimer;
@end
#import "LHTimer.h"

@implementation LHTimer {
    
    NSTimer *_timer;
}

- (void)startTimer
{
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

- (void)destroyTimer
{
    
    if (!_timer) {
        return;
    }
    [_timer invalidate];
    _timer = nil;
}

- (void)timerAction
{
    NSLog(@"timerAction");
}

- (void)dealloc
{
    
    [_timer invalidate];
    _timer = nil;
}

@end

使用时:

#import "ViewController.h"
#import "LHTimer.h"

@interface ViewController ()

@property (nonatomic, strong) LHTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"Timer VC";
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.timer = [[LHTimer alloc] init];
    [timer startTimer];
}

- (void)dealloc 
{   
    [self.timer destroyTimer];
}

这里的做法是将原来的timer和VC之间的强引用变成LHTimer和timer之间的强引用,避免timer直接强引用self代表的VC。然而,细心之下你还会发现两个问题:

  • 二次封装提升代码的耦合度
  • 即使封装也要将ti,target,selector,userInfo,repeat这些参数预留出来,供不同地方使用

3)iOS10之后的新API

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

+ (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));

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

新的API可以不用那么麻烦的去viewWillDisappear里释放,也不用封装,通过__weak弱引用self,可以避免循环引用,这时候就不是NSTimer的问题,而是block怎么避免循环引用了。

4)iOS10之前的API自己改造成block

iOS10之前的API,我们也经常将它封装成一个block形式的API,利用这种方式,可以达到上一条中的效果,当然,要做的事情肯定会多一些,为了详细说明,这里再贴下代码:

#import <Foundation/Foundation.h>

@interface NSTimer (LHTimer)

+ (NSTimer *)LH_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats;

@end
#import "NSTimer+LHTimer.h"

@implementation NSTimer (LHTimer)

+ (NSTimer *)LH_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats 
{
    return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(handle:) userInfo:[block copy] repeats:repeats];
}

+ (void)handle:(NSTimer *)timer 
{
    void(^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}
@end

注意事项:

  • copy是为防止block在需要的时候已经销毁掉,所以需要拷贝到堆上
  • 使用时记得用__weak避免循环引用 
  • 使用该方案需要引入此类头文件
  • 这是一个categroy,categroy可以给系统类添加新方法

5)使用NSProxy增加一个中间层subTarget

其原理是利用NSProxy给NSTimer加入一层stubTarget,stubTarget主要做为一个桥接层,负责NSTimer和target之间的通信。同时NSTimer强引用stubTarget,而stubTarget弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。

这个方式的好处是,当NSTimer的回调函数fireProxyTimer:被执行的时候,会自动判断原target是否已经被释放,如果释放了,意味着NSTimer已经无效,虽然如此,但还是要在不需要的地方进行invalidate操作,只是不需要置nil。

下面看代码:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LHProxy : NSObject

//通过实例方法创建对象
- (instancetype)initWithObjc:(id)object;

//通过类方法创建对象
+ (instancetype)proxyWithObjc:(id)object;


@end

NS_ASSUME_NONNULL_END
#import "LHProxy.h"

@interface LHProxy()

@property (nonatomic, weak) id subTarget;

@end

@implementation LHProxy

- (instancetype)initWithObjc:(id)object {
    
    self.subTarget = object;
    return self;
}

+ (instancetype)proxyWithObjc:(id)object {
    
    return [[self alloc] initWithObjc:object];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    
    if ([self.subTarget respondsToSelector:invocation.selector]) {
        
        [invocation invokeWithTarget:self.subTarget];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    
    return [self.subTarget methodSignatureForSelector:sel];
}

@end

调用:

#import "ViewController1.h"
#import "LHProxy.h"

@interface ViewController1 ()
@property (nonatomic, strong)NSTimer *timer;
@end

@implementation ViewController1

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

    LHProxy *proxy = [[LHProxy alloc] initWithObjc:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(action) userInfo:nil repeats:YES];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissAction)];
    [self.view addGestureRecognizer:tap];
}

- (void)dismissAction {
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (void)action {
    NSLog(@"1111111");
}

- (void)dealloc {
    [self.timer invalidate];
}

@end

这里是将timer的target对象转移到其他对象,避免真正使用timer的对象被timer强引用(其实源头是被runloop强引用着)。这时,可以在真正使用timer对象的dealloc方法中调用timer的invalidate方法,来解除runloop对timer的强引用,进而释放timer对象。这一点和NSTimer的block用法类似。


总结:NSTimer的问题是在什么时候对其进行invalidate和置nil的问题,普通的NSTimer,放在dealloc内处理,因为self被NSTimer强引用,需要等到NSTimer被invalidate后才能释放,继而执行dealloc方法,而NSTime的invalidate方法则在dealloc内,这就造成了相互等待,无法释放的问题。掌握了这一点,NSTimer就不再再造成内训泄露和循环引用了。这里要格外注意一点,不管是哪种方式,最终都要调用invalidate方法,我们封装或者改造过的NSTimer只是可以不用做置nil操作,同时,可以写在dealloc内

相关文章:

  • 【电商】电商后台设计—售后流程
  • 2022:【例4.7】最小n值
  • CDH 08Cloudera Manager freeIPAKerberos安装配置
  • steam搬砖基础分析
  • PMP每日一练 | 考试不迷路-9.24(包含敏捷+多选)
  • 重写,重载,重定义习题
  • 8-Arm PEG-Acrylate,8-Arm PEG-AC,八臂-聚乙二醇-丙烯酸酯长期供应
  • qt 构建路径设置
  • 2022-09-23 答辩准备
  • 网课搜题接口API
  • Golang操作ES
  • Activity在Create阶段做的你不知道的事看源码
  • java编写一个程序,生成0~9之间的100个随机数字并且统计每一个数字的产生次数。
  • 【day11】LeetCode(力扣)练习【1652.拆炸弹】【235. 二叉搜索树的最近公共祖先】【733. 图像渲染】
  • et文件丢失怎么恢复?5种恢复方法轻松掌握
  • 2017-08-04 前端日报
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • Android开源项目规范总结
  • CNN 在图像分割中的简史:从 R-CNN 到 Mask R-CNN
  • C语言笔记(第一章:C语言编程)
  • Docker 1.12实践:Docker Service、Stack与分布式应用捆绑包
  • Joomla 2.x, 3.x useful code cheatsheet
  • js对象的深浅拷贝
  • LeetCode算法系列_0891_子序列宽度之和
  • npx命令介绍
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • SAP云平台里Global Account和Sub Account的关系
  • sessionStorage和localStorage
  • Spring Boot MyBatis配置多种数据库
  • Spring框架之我见(三)——IOC、AOP
  • Vue--数据传输
  • vue--为什么data属性必须是一个函数
  • 编写高质量JavaScript代码之并发
  • 手机app有了短信验证码还有没必要有图片验证码?
  • 项目管理碎碎念系列之一:干系人管理
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​LeetCode解法汇总2670. 找出不同元素数目差数组
  • #stm32驱动外设模块总结w5500模块
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (2009.11版)《网络管理员考试 考前冲刺预测卷及考点解析》复习重点
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (第61天)多租户架构(CDB/PDB)
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (一)Java算法:二分查找
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • (转)Oracle 9i 数据库设计指引全集(1)
  • .describe() python_Python-Win32com-Excel
  • .Mobi域名介绍
  • .NET BackgroundWorker
  • .NET CLR Hosting 简介
  • .net mvc actionresult 返回字符串_.NET架构师知识普及
  • .NET 反射的使用
  • .NET和.COM和.CN域名区别
  • .NET中GET与SET的用法
  • .net最好用的JSON类Newtonsoft.Json获取多级数据SelectToken