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(_