iOS NSTimer 循環引用

NSTimer

我們常常用NSTimer做一些定時任務,代碼如下:

#import "YTimerVC.h"

@interface YTimerVC ()
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation YTimerVC

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeAction) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timeAction {
    static int i = 0;
    NSLog(@"%d", i++);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}


問題:當我們返回上個頁面的時候,返現dealloc方法並沒有走。造成ViewController內存泄漏。
循環引用圖
在這裏插入圖片描述
當我們放回上個頁面的時候,由於Timer添加到RunLoop執行,Timer不會釋放TimerVC的持有。

最簡單的方法一

在ViewController,視圖消失的時候,我們調用Timerinvalidate,讓RunLoop釋放對Timer的持有,響應的Timer也就釋放對VC的持有

缺點:當我們在自定義View的視圖中也添加Timer定期器任務,我們要做什麼時候釋放呢?也許,可以再Timer屬性暴露給VC使用,這樣的代碼耦合度比較高,不建議使用。

方法二

顯然這種方式是不能打破,放回上一級,VC釋放掉。

@property (nonatomic, weak) NSTimer *timer;

在這裏插入圖片描述

高逼格方法:NSProxy

從右邊入手,如果我們能讓Timer持有VC變爲弱引用的話,那麼當返回上一級的時候就不會出現這種情況。
NSProxyApple文檔說明,簡單來說提供了幾個信息

  1. NSProxy是一個專門用來做消息轉發的類
  2. NSProxy是個抽象類,使用需自己寫一個子類繼承自NSProxy
  3. NSProxy的子類需要實現兩個方法methodSignatureForSelectorforwardInvocation

我們知道了解了OC中消息轉發的機制,那麼你肯定知道,當某個對象的方法找不到的時候,也就是最後拋出doesNotRecognizeSelector的時候

  1. 消息發送,從方法緩存中找方法,找不到去方法列表中找,找到了將該方法加入方法緩存,還是找不到,去父類裏重複前面的步驟,如果找到底都找不到那麼進入2。

  2. 動態方法解析,看該類是否實現了resolveInstanceMethod:和resolveClassMethod:,如果實現了就解析動態添加的方法,並調用該方法,如果沒有實現進入3。

  3. 消息轉發,這裏分二步

    1. 調用forwardingTargetForSelector:,看返回的對象是否爲nil,如果不爲nil,調用objc_msgSend傳入對象和SEL
    2. 如果上面爲nil,那麼就調用methodSignatureForSelector:返回方法簽名,如果方法簽名不爲nil,調用forwardInvocation:來執行該方法
  4. 如果以上都沒有處理:doesNotRecognizeSelector

NSProxy代碼實現:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface YProxy : NSProxy
@property (nonatomic, weak) id target;
@end

NS_ASSUME_NONNULL_END

#import "YProxy.h"
@implementation YProxy
/**
 objc_methodSend(id, SEL,...)
 
 流程:resolveInstanceMethod/resolveClassMethod
 如果沒有響應:forwardingTargetForSelector //轉發tagret,切記不能是self,會造成循環
 如果設置爲Nil:methodSingnatureForSelector
 -> forwardInvocation
 
 */
///先走這個:
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

///如果forwardingTargetForSelector 爲空的話,我們就用NSOjbect初始化一個init
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
///如果聲明一個null對象,並返回[invocation getReturnValue:&null]。讓null去響應這樣,就算我們不賦值taget 也不會出現崩潰現象
- (void)forwardInvocation:(NSInvocation *)invocation {
//    [invocation invokeWithTarget:self.target];
    void *null = NULL;
    [invocation getReturnValue:&null];
}
@end


//controller代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    YProxy *proxy = [YProxy alloc];
    proxy.target = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(timeAction) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timeAction {
    static int i = 0;
    NSLog(@"%d", i++);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}

引用流程圖:

在這裏插入圖片描述

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