NSTimer循環引用分析,解決

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];
}

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章