首先介紹NSTimer的幾種創(chuàng)建方式
- (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;
三種方法的區(qū)別是:
- scheduledTimerWithTimeInterval方法不僅創(chuàng)建了NSTimer對(duì)象,還把該NSTimer對(duì)象加入到了當(dāng)前的RunLoop(默認(rèn)NSDefaultRunLoopModel模式)中冰更。
- 前兩個(gè)方法需要使用addTimer:forMode:方法將NSTimer加入到RunLoop中。
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
與UIScrollView使用時(shí)注意事項(xiàng)
在當(dāng)前線(xiàn)程為主線(xiàn)程時(shí)扯再,某些UI事件煌妈,比如UIScrollView的拖動(dòng)操作,會(huì)將RunLoop切換為NSEventTrackingRunLoopModel模式冯勉,在這個(gè)過(guò)程中萌壳,默認(rèn)的NSDefaultRunLoopModel模式中注冊(cè)的事件是不會(huì)被執(zhí)行的亦镶。
這時(shí)可以將Timer按照NSRunLoopCommonModes模式加入到RunLoop中日月。
通常情況下NSDefaultRunLoopMode和UITrackingRunLoopMode都已經(jīng)被加入到了common modes集合中, 所以不論runloop運(yùn)行在哪種mode下, NSTimer都會(huì)被及時(shí)觸發(fā)
如何銷(xiāo)毀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方法會(huì)停止計(jì)時(shí)器的再次觸發(fā),并在RunLoop中將其移除染乌。
- invalidate方法是將NSTimer對(duì)象從RunLoop中移除的唯一方法山孔。
- 調(diào)用invalidate方法會(huì)刪除RunLoop對(duì)NSTimer的強(qiáng)引用,以及NSTimer對(duì)target和userInfo的強(qiáng)引用荷憋!
那為什么RunLoop會(huì)對(duì)NSTimer強(qiáng)引用呢台颠?
Timers work in conjunction with run loops. Run loops maintain strong references to their timers
( 計(jì)時(shí)器與運(yùn)行循環(huán)一起工作。運(yùn)行循環(huán)維護(hù)對(duì)計(jì)時(shí)器的強(qiáng)引用)
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.
(當(dāng)計(jì)時(shí)器觸發(fā)后勒庄,在調(diào)用invalidated之前會(huì)一直保持對(duì)target的強(qiáng)引用)
以上也解釋了下面要說(shuō)的NSTimer造成循環(huán)引用的原因
循環(huán)引用造成內(nèi)存泄漏
由上可見(jiàn):NSTimer強(qiáng)引用了self串前,self也強(qiáng)引用了NSTimer,由此造成了循環(huán)引用实蔽,同時(shí)Runloop也強(qiáng)引用NSTimer荡碾。
- 下面介紹兩種情況下解決循環(huán)引用
- 一般情況下直接在vc的viewWillDisappear中調(diào)用以下方法即可解決
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
- 在A--push--B,B返回A
這種情況顯然是在dealloc中調(diào)用invalidate方法局装,
有些人可能會(huì)想將NSTimer弱引用
@property (nonatomic, weak)NSTimer *timer;
- 但是RunLoop強(qiáng)引用了timer ~坛吁,timer強(qiáng)引用了vc,所以dealloc不會(huì)被調(diào)用铐尚!
- 或者target傳入weakSelf拨脉,由于在invalidate方法調(diào)用之前,timer一直強(qiáng)引用target宣增,而強(qiáng)引用了弱引用所引用的對(duì)象玫膀,等價(jià)于強(qiáng)引用!
下面介紹幾種成熟的解決方案
一. 使用自定義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
定時(shí)器對(duì)象指定的target是NSTimer類(lèi)對(duì)象是個(gè)單例爹脾,因此計(jì)時(shí)器是否會(huì)保留它都無(wú)所謂帖旨。這么做,循環(huán)引用依然存在灵妨,但是因?yàn)轭?lèi)對(duì)象無(wú)需回收解阅,所以能解決問(wèn)題。
優(yōu)點(diǎn):代碼簡(jiǎn)潔泌霍,邏輯清晰
缺點(diǎn):
1.需要使用weakSelf避免block循環(huán)引用
2.不再使用原生API
3.同時(shí)要為NSTimer何CADisplayLink分別引進(jìn)一個(gè)Category
二. GCD自己實(shí)現(xiàn)Timer
直接用GCD自己實(shí)現(xiàn)一個(gè)定時(shí)器货抄,YYKit直接有一個(gè)現(xiàn)成的類(lèi)YYTimer這里不再贅述。
缺點(diǎn):代價(jià)有點(diǎn)大烹吵,需要自己重新造一個(gè)定時(shí)器碉熄。
三. 代理NSProxy
使用工具類(lèi)YYWeakProxy解決NSTimer/CADisplayLink循環(huán)引用問(wèn)題桨武!
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
該方法引入一個(gè)YYWeakProxy對(duì)象肋拔,在這個(gè)對(duì)象中弱引用真正的目標(biāo)對(duì)象。通過(guò)YYWeakProxy對(duì)象呀酸,將NSTimer/CADisplayLink對(duì)象弱引用目標(biāo)對(duì)象凉蜂。
使用方法:
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;// 對(duì)象置nil是一種規(guī)范和習(xí)慣
}
為什么NSProxy的子類(lèi)YYWeakProxy可以解決呢?
- NSProxy本身是一個(gè)抽象類(lèi),它遵循NSObject協(xié)議窿吩,提供了消息轉(zhuǎn)發(fā)的通用接口茎杂,NSProxy通常用來(lái)實(shí)現(xiàn)消息轉(zhuǎn)發(fā)機(jī)制和惰性初始化資源。不能直接使用NSProxy纫雁。需要?jiǎng)?chuàng)建NSProxy的子類(lèi)煌往,并實(shí)現(xiàn)init以及消息轉(zhuǎn)發(fā)的相關(guān)方法,才可以用轧邪。
- YYWeakProxy繼承了NSProxy刽脖,定義了一個(gè)弱引用的target對(duì)象,通過(guò)重寫(xiě)消息轉(zhuǎn)發(fā)等關(guān)鍵方法忌愚,讓target對(duì)象去處理接收到的消息曲管。在整個(gè)引用鏈中,Controller對(duì)象強(qiáng)引用NSTimer/CADisplayLink對(duì)象硕糊,NSTimer/CADisplayLink對(duì)象強(qiáng)引用YYWeakProxy對(duì)象院水,而YYWeakProxy對(duì)象弱引用Controller對(duì)象,所以在YYWeakProxy對(duì)象的作用下简十,Controller對(duì)象和NSTimer/CADisplayLink對(duì)象之間并沒(méi)有相互持有檬某,完美解決循環(huán)引用的問(wèn)題。
參考文檔
1.iOS實(shí)錄8:解決NSTimer/CADisplayLink的循環(huán)引用
2.NSTimer Class