iOS NSTimer定时器
Posted 长沙火山
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS NSTimer定时器相关的知识,希望对你有一定的参考价值。
ios中定时器有三种,分别是NSTimer、CADisplayLink、dispatch_source,下面就分别对这三种计时器进行说明。
一、NSTimer
NSTimer这种定时器用的比较多,但是特别需要注意释放问题,如果处理不好很容易引起循环引用问题,造成内存泄漏。
1.1 NSTimer的创建
NSTimer有两种创建方法。
方法一:
这种方法虽然创建了NSTimer,但是定时器却没有起作用。这种方式创建的NSTimer,需要加入到NSRunLoop中,有NSRunLoop的驱动才会让定时器跑起来。
self.timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
方法二:
这种方法创建的NSTimer不需要加入到NSRunLoop中就能跑起来,因为创建了定时器之后,系统默认会把定时器加到RunLoop中,不需要我们自己手动加了。
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
1.2 NSTimer的释放
- (void)dealloc
[self.timer invalidate];
self.timer = nil;
NSLog(@"dealloc...");
通常这样写其实是有问题的,由于self强引用了timer,同时timer也强引用了self,所以循环引用造成dealloc方法根本不会走,self和timer都不会被释放,造成内存泄漏。
二、循环引用的几种解决办法
3.1 使用invalidate结束timer运行
我们第一时间肯定想到的是[self.timer invalidate]
不就可以了吗,当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear
还是viewDidDisAppear
?实际上在我们实际操作中,如果当前页面有push操作的话,当前页面还在栈里面,这时候我们释放timer肯定是错误的,所以这时候我们可以用到下面的方法:
- (void)didMoveToParentViewController:(UIViewController *)parent
// 无论push 进来 还是 pop 出去 正常运行
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil)
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer销毁");
3.2 中介者模式
换个思路,timer会造成循环引用是因为target强持有了self,造成的循环引用,那我们是否可以包装一下target,使得timer绑定另外一个不是self的target对象来打破这层强持有关系。
@interface HSTimer : NSObject
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
#import "HSTimer.h"
@interface HSTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation HSTimerTarget
- (void)timeAction:(NSTimer *)timer
if(self.target)
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
else
[self.timer invalidate];
@end
@implementation HSTimer
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats
HSTimerTarget* timerTarget = [[HSTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(timeAction:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
@end
使用:
#import "HSTimer.h"
@property(nonatomic, strong) NSTimer *timer;
self.timer = [HSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
3.3 NSProxy虚基类的方式
NSProxy是一个虚基类,它的地位等同于NSObject。我们不用self来响应timer方法的target,而是用NSProxy来响应。
HSProxy.h
@interface HSProxy : NSObject
+ (instancetype)proxyWithTransformObject:(id)object;
@end
HSProxy.m
#import "HSProxy.h"
@interface HSProxy ()
@property (nonatomic, weak) id object;
@end
@implementation HSProxy
+ (instancetype)proxyWithTransformObject:(id)object
HSProxy *proxy = [HSProxy alloc];
proxy.object = object; // 我们拿到外边的self,weak弱引用持有
return proxy;
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// proxy虚基类并没有持有vc,而是消息的转发,又给了vc
- (id)forwardingTargetForSelector:(SEL)aSelector
return self.object;
@end
使用
#import "HSProxy.h"
@property(nonatomic, strong) HSProxy *proxy;
self.proxy = [HSProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self.proxy selector:@selector(timeEvent) userInfo:nil repeats:YES];
三、拓展
3.1 能不能用weakSelf打破循环引用?
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:weakSelf selector:@selector(timeEvent) userInfo:nil repeats:YES];
答案:不能。
weakSelf 和 self 虽然指针地址不一样,但是都是指向当前的vc,也就是说这两者指向的内存地址其实是一样的,所以传入target里面的其实还是当前的vc,这样还是不能打破循环引用。
3.2 滑动tableView或者scrollView会暂停NSTimer
在同一个ViewController里面创建NSTimer和tableView或者scrollView,当滑动tableView或者scrollView的时候,会发现NSTimer会暂停,当tableView或者scrollView停止之后,NSTimer又会继续运行。这是为什么呢?
RunLoop在不同的情况下,运行的模式不一样:
在viewDidApperar()方法中打印的Current Mode为:kCFRunLoopDefaultMode
在scrollViewDidScroll()方法中打印的Current Mode为:UITrackingRunLoopMode
因为NSTimer对象默认添加到了当前RunLoop的DefaultMode中,而在切换成TrackingRunLoopMode时,定时器就会停止工作。
解决该问题最直接方法是,将NSTimer在TrackingRunLoopMode中也添加一份。这样的话无论是在 DefaultMode还是TrackingRunLoopMode中,定时器都会正常的工作。
如果你对RunLoop比较熟悉的话,可以知道CommonModes就是DefaultMode和TrackingRunLoopMode的集合,所以我们只需要将NSTimer对象与当前线程所对应的RunLoop中的CommonModes关联即可。
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
3.3 NSTimer精度不高
NSTimer计时的精度不是很高,但是项目中一般的需求还是能满足,如果需要更高的计时精度就需要用CADisplayLink。
以上是关于iOS NSTimer定时器的主要内容,如果未能解决你的问题,请参考以下文章