目录
1.NSTimer导致的循环引用分析
2.NSTimer循环引用解决思路误区
3.NSTimer循环引用解决方案
4.NSTimer不准确的问题探究及解决
1.NSTimer导致的循环引用分析
- CADisplayLink(频率能达到屏幕刷新率的定时器类)也和NSTimer一样会有此问题,这里为了方便只使用NSTimer去讲解。
如下面代码 在控制器创建一个NSTimer定时器:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
当我们退出控制器会发现dealloc没有被调用,timerTest还是一直在执行,即明显的发生了内存泄漏,原因如下:
- timer是ViewController的成员,即ViewController对timer是强引用;
-
NSTimer创建会对target传入对象产生强引用,此时我们传入了self(即是ViewController),即NSTimer对self也是强引用,关系如下图
截屏2021-02-25 下午7.33.01.png
所以他们两者相互被强引用,即发生了循环引用,dealloc 永远不会被执行,timer 也永远不会被释放,造成内存泄漏。
2.NSTimer循环引用解决思路误区
当我们遇到循环引用大多数第一反应就是使用weak解决,我们来看一下
- 一:把timer改成弱引用让self对timer是弱引用
@property (weak, nonatomic) NSTimer *timer;
虽然self对timer是弱引用,但是timer销毁在dealloc方法里面执行,而dealloc又需要timer销毁才能执行,即两者相互等待,循环引用问题依旧存在。
- 二:使用__weak self,让timer弱引用self
weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。
- 三 : 在“适当的时机释放定时器”
比如在页面即将消失的时候:
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
在某些情况下,这种做法是可以解决问题的,但是有时却会引起其他问题,比如控制器push到下一个控制器,viewDidDisappear执行后,timer被释放,此时再pop回来,timer已经不复存在了。
所以,这种"方案"并不是合理的。
优化上面的方法这个时候可以采用配对使用在 viewWillAppear 开timer启,在 viewWillDisappear 关闭timer:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if (!self.timer) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
虽然能解决问题,但是在项目中,每次使用定时器都写这么一堆,显得不够优雅,维护起来也比较麻烦。而且有的业务场景这么使用依旧会产生各种问题。
NSTimer循环引用解决方案:
1、使用block的创建方式解决:
iOS10中,NSTimer的API新增了block方法,我们可以直接使用这种方式创建定时器结合weak使用即可
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
2、给self添加中间对象TimerPoxy
引入一个中间对象TimerPoxy,TimerPoxy弱引用 self,然后 TimerPoxy 传入NSTimer。即self 强引用NSTimer,NSTimer强引用 TimerPoxy,TimerPoxy 弱引用 self,这样没有相互强引用,则不会造成循环引用。
- TimerPoxy的实现:
TimerProxy.h
#import
@interface TimerProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
TimerProxy.m
#import "TimerProxy.h"
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target
{
TimerProxy *proxy = [[TimerProxy alloc] init];
proxy.target = target;
return proxy;
}
/*
因为这里并没有实现定时器的timerTest方法,
timerTest方法是在ViewController里面实现的,
所以这里我们要利用消息转发机制,把方法交给ViewController去实现
**/
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
本文中由于当前target是TimerProxy,但是TimerProxy没有实现timerTest方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找ViewController中的方法实现。
- ViewController中的实现:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:[TimerProxy proxyWithTarget:self]
selector:@selector(timerTest)
userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
这样就实现上面图示的内容,解决了循环引用的问题。
优化解决方案:
- 不使用NSObject类实现,使用NSProxy类去实现。
上面我们用NSObject实现转发定时器的过程大概是:
当前类查找timerTest实现方法 ---> NSObject 类找timerTest实现方法 --->消息转发至ViewController查找实现方法。
而NSProxy一般就是用来做消息转发的,大概过程:
直接把实现方法转发ViewController。
这样对比下来,NSProxy比NSObject效率高且节省资源
NSProxy实现:
#import
@interface TimerProxys : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "TimerProxys.h"
@implementation TimerProxys
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
TimerProxys *proxy = [TimerProxys alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
到此定时器的循环引用问题,已经完美解决啦~
4.NSTimer不准确的问题探究及解决
为什么NSTimer会不准确?
1、NSTimer定时器依赖于RunLoop,我们知道RunLoop每循环一次的时间是基于任务量的,即每一次的时间都不一定相同,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。
2、模式的切换,当创建的timer被加入到NSDefaultRunLoopMode时,此时如果有滑动UIScrollView的操作,runLoop 的mode会切换为TrackingRunLoopMode,而MainRunLoop处于 UITrackingRunLoopMode 的模式下,是不会处理 NSDefaultRunLoopMode 的消息(因为它们的RunLoop Mode不一样),所以timer会暂时停止;
对于精度要求不高的场景我们使用NSTimer没太大影响,对于上面2的滑动UIScrollView的导致定时器停止的问题下面一行代码即可解决:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- 解决方案:
使用GCD定时器,因为GCD定时器是基于系统内核的,所以不会受其他因素影响,基本使用方法这里不作讲解,github有相关封装demon,有需要自取。
本文代码demon地址:https://github.com/yizhixiafancai/timerAbout
点个赞再走呗~