NSTimer常見用法
@interface TimerClass : NSObject
- (void)start;
- (void)stop;
@end
@implementation TimerClass {
NSTimer *_timer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
NSLog(@"%s",__func__);
}
- (void)stop {
[_timer invalidate];
_timer = nil;
}
- (void)start {
_timer = [NSTimerscheduledTimerWithTimeInterval:5.0
target:self
selector:selector(doSomething)
userInfo:nil
repeats:YES];
}
- (void)doSomething {//doSomething}
@end
上面代碼很容易理解成
self 持有成員變量 NSTimer,
成員變量
NSTimer 又強引用了 TimerClass 實例,才導致循環引用。
NSTimer思考
思考示例1
self 持有成員變量 NSTimer, 我們試着如果NSTimer
不是成員變量,self沒有持有成員變量,delloc方法會調用嗎?
- (void)test {
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(doSomething)
userInfo:nil
repeats:YES];
}
- (void)doSomething {}
- (void)dealloc { NSLog(@"%s",__func__);}
思考示例2
成員變量NSTimer
又強引用了 TimerClass 實例 self ,那麼如果向NSTimer中傳入 __weak 修飾符修飾的self實例呢,delloc方法會調用嗎??
__weak typeof(self) weakSelf = self;
self.mytimer = [NSTimer scheduledTimerWithTimeInterval:1
target:weakSelf
selector:@selector(doSomeThing)
userInfo:nil
repeats:YES];
在一個Controller加入該代碼,我們就會發現dealloc
都沒有調用了。
那麼很明顯
TimerClass實例持有了,成員變量 NSTimer,成員變量 NSTimer 又強引用了 TimerClass 實例,才導致循環引用。
定時器加在
runloop
上纔會起作用,到達時間點後就會執行action
方法,並且可以肯定這是一個對象方法
。 定時器運行在主線程的runloop
上,然後又回調方法,這個方法屬於你當前這個VC的對象方法
。既然是對象方法且能被調用,那麼肯定所屬的對象一定的要持有,因此這個對象被持有了。而我們通過
__weak
修飾self
,依然不能打破這個循環引用,說明這個對象依然是被強引用。定時器加在 runloop 上, runloop 是持有定時器的,當不移除定時器且 runloop 一直存在的話那麼每隔一段時間就會調用 action 這個方法,既然要調用這個對象方法,就需要佔有這個對象。所以導致當前控制器VC不被釋放,也證明了 局部變量的 NSTimer 造成循環引用的原因。
其實我們可以開啓子線程的runloop
, 添加定時器,通過終止子線程 runloop
,就能驗證這個問題。
從runloop
中把 NSTimer
移除 /終止 runloop
解決NSTimer循環引用的方法有三種
- 使用類方法
- 使用weakProxy
- 使用GCD timer
weakProxy 解決循環引用
==NSProxy== 本身是一個抽象類,它遵循NSObject協議,提供了消息轉發的通用接口。==NSProxy== 通常用來實現消息轉發機制和惰性初始化資源。
@interface JZWeakProxy()
@property (nonatomic, weak, readonly) id target;
@end
@implementation JZWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[self alloc] initWithTarget:target];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = [invocation selector];
if([self.target respondsToSelector:sel]){
[invocation invokeWithTarget:self.target];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSAssert(self.target, @"目前對象失效, NSTimer必須註銷");
return [self.target methodSignatureForSelector:sel];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self.target respondsToSelector:aSelector];
}
@end
//VC
- (void)viewDidLoad{
[super viewDidLoad];
_timer = [NSTimer timerWithTimeInterval:1
target:[JZWeakProxy proxyWithTarget:self]
selector:@selector(doSomeThing)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)doSomeThing{
NSLog(@"=====================");
}
- (void)dealloc{
[_timer invalidate];
_timer = nil;
NSLog(@"%s",__func__);
}
通過使用NSProxy類
,消息轉發的接口,改變實例方法doSomeThing
的調用者.
綜合上述所說,NSTime
勢必持有 JZWeakProx
實例對象, 然後結合消息轉發
,改變實例方法的調用
者,從而實現 Controller
調用實例方法,JZWeakProxy
實例對象又不強引用 Controller實例
,那麼 Controller實例
能夠正常釋放。
注意
當 控制器實例 釋放後,我們必須去是註銷 NSTimer,即調用 invalidate 方法,否則拋出以下異常
Trapped uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy doesNotRecognizeSelector:doSomeThing] called!'
原因很簡單,因爲調用者已經被釋放,doSomeThing
事件沒有調用實例。
Block解決循環引用
@interface NSTimer (JZBlocksSupport)
+ (NSTimer *)jz_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
@implementation NSTimer (JZBlocksSupport)
+ (NSTimer *)jz_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(jz_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)jz_blockInvoke:(NSTimer *)timer {
void (^block)() = timer.userinfo;
if(block) {
block();
}
}
@end
//調用
- (void)start {
__weak JZClass *weakSelf = self;
_timer = [NSTimer xx_scheduledTimerWithTimeInterval:.5
block:^{
JZClass *strongSelf = weakSelf;
[strongSelf doSomething];
}
repeats:YES];
}