首先介绍NSTimer的几种创建方式
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep
+ (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;
三种方法的区别是:
- scheduledTimerWithTimeInterval方法不仅创建了NSTimer对象,还把该NSTimer对象加入到了当前的RunLoop(默认NSDefaultRunLoopModel模式)中。
- 前两个方法需要使用addTimer:forMode:方法将NSTimer加入到RunLoop中。
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
与UIScrollView使用时注意事项
在当前线程为主线程时,某些UI事件,比如UIScrollView的拖动操作,会将RunLoop切换为NSEventTrackingRunLoopModel模式,在这个过程中,默认的NSDefaultRunLoopModel模式中注册的事件是不会被执行的。
这时可以将Timer按照NSRunLoopCommonModes模式加入到RunLoop中。
通常情况下NSDefaultRunLoopMode和UITrackingRunLoopMode都已经被加入到了common modes集合中, 所以不论runloop运行在哪种mode下, NSTimer都会被及时触发
如何销毁NSTimer
invalidate方法的官方介绍:
Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoopobject. The NSRunLoop
object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
意思是:
- invalidate方法会停止计时器的再次触发,并在RunLoop中将其移除。
- invalidate方法是将NSTimer对象从RunLoop中移除的唯一方法。
- 调用invalidate方法会删除RunLoop对NSTimer的强引用,以及NSTimer对target和userInfo的强引用!
那为什么RunLoop会对NSTimer强引用呢?
Timers work in conjunction with run loops. Run loops maintain strong references to their timers
( 计时器与运行循环一起工作。运行循环维护对计时器的强引用)
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
(当计时器触发后,在调用invalidated之前会一直保持对target的强引用)
以上也解释了下面要说的NSTimer造成循环引用的原因
循环引用造成内存泄漏
由上可见:NSTimer强引用了self,self也强引用了NSTimer,由此造成了循环引用,同时Runloop也强引用NSTimer。
- 下面介绍两种情况下解决循环引用
- 一般情况下直接在vc的viewWillDisappear中调用以下方法即可解决
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
- 在A--push--B,B返回A
这种情况显然是在dealloc中调用invalidate方法,
有些人可能会想将NSTimer弱引用
@property (nonatomic, weak)NSTimer *timer;
- 但是RunLoop强引用了timer ~,timer强引用了vc,所以dealloc不会被调用!
- 或者target传入weakSelf,由于在invalidate方法调用之前,timer一直强引用target,而强引用了弱引用所引用的对象,等价于强引用!
下面介绍几种成熟的解决方案
一. 使用自定义Category用Block解决
NSTimer+ZHWeakTimer.h
@interface NSTimer (ZHWeakTimer)
+ (NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void (^)(void))eventBlock repeats:(BOOL)repeats;
@end
NSTimer+ZHWeakTimer.m
@implementation NSTimer (ZHWeakTimer)
+ (NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void (^)(void))eventBlock repeats:(BOOL)repeats
{
NSTimer *timer = [self scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(zh_executeTimer:) userInfo:[eventBlock copy] repeats:repeats];
return timer;
}
+ (void)zh_executeTimer:(NSTimer *)timer
{
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
定时器对象指定的target是NSTimer类对象是个单例,因此计时器是否会保留它都无所谓。这么做,循环引用依然存在,但是因为类对象无需回收,所以能解决问题。
优点:代码简洁,逻辑清晰
缺点:
1.需要使用weakSelf避免block循环引用
2.不再使用原生API
3.同时要为NSTimer何CADisplayLink分别引进一个Category
二. GCD自己实现Timer
直接用GCD自己实现一个定时器,YYKit直接有一个现成的类YYTimer这里不再赘述。
缺点:代价有点大,需要自己重新造一个定时器。
三. 代理NSProxy
使用工具类YYWeakProxy解决NSTimer/CADisplayLink循环引用问题!
YYWeakProxy.h
@interface YYWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
-(instancetype)initWithTarget:(id)target;
+(instancetype)proxyWithTarget:(id)target;
@end
YYWeakProxy.m
-(instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+(instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
-(id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
-(void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
-(BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
-(BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
-(NSUInteger)hash {
return [_target hash];
}
-(Class)superclass {
return [_target superclass];
}
-(Class)class {
return [_target class];
}
-(BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
-(BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
-(BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
-(BOOL)isProxy {
return YES;
}
-(NSString *)description {
return [_target description];
}
-(NSString *)debugDescription {
return [_target debugDescription];
}
@end
该方法引入一个YYWeakProxy对象,在这个对象中弱引用真正的目标对象。通过YYWeakProxy对象,将NSTimer/CADisplayLink对象弱引用目标对象。
使用方法:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:[YYWeakProxy proxyWithTarget:self]
selector:@selector(timeEvent)
userInfo:nil
repeats:YES];
- (void)timeEvent{
}
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;// 对象置nil是一种规范和习惯
}
为什么NSProxy的子类YYWeakProxy可以解决呢?
- NSProxy本身是一个抽象类,它遵循NSObject协议,提供了消息转发的通用接口,NSProxy通常用来实现消息转发机制和惰性初始化资源。不能直接使用NSProxy。需要创建NSProxy的子类,并实现init以及消息转发的相关方法,才可以用。
- YYWeakProxy继承了NSProxy,定义了一个弱引用的target对象,通过重写消息转发等关键方法,让target对象去处理接收到的消息。在整个引用链中,Controller对象强引用NSTimer/CADisplayLink对象,NSTimer/CADisplayLink对象强引用YYWeakProxy对象,而YYWeakProxy对象弱引用Controller对象,所以在YYWeakProxy对象的作用下,Controller对象和NSTimer/CADisplayLink对象之间并没有相互持有,完美解决循环引用的问题。