解決NSTimer迴圈引用導致記憶體洩漏的六種方法

BetterDays發表於2018-06-19

demo放在了GitHub

參考另一篇文章可加深印象NSTimer的八種建立方式

另一篇文章對第三種方法做了重點解釋讓大家拿過來就能用且沒有迴圈引用的定時器TFQWeakTimer

記憶體洩漏的原因:

self強引用timertimer新增在runloop上,只要timer不銷燬self就銷燬不了。當然了你可以選擇在viewWillDisappear中銷燬timer。但是定時器頁面不一定都是pop到上一個頁面,也有可能push新頁面,也有可能是進入後臺,這樣我們希望重新回到定時器頁面的時候,定時任務還依舊是執行狀態。所以invalidate放到viewWillDisappear是不合理的,唯一合理的地方就是定時器頁面銷燬的時候銷燬timer。這就引出了以下三種解決方法。

第一、二種方法是在合適的時機銷燬timer,幹掉強引用。

第三種方法是自定義timer,弱引用timer,從源頭上就不形成迴圈引用,更不會導致記憶體洩漏。

一、離開當前控制器銷燬NSTimer

didMoveToParentViewController方法瞭解一下

@interface WLZSecondController ()

@property (nonatomic, strong)UILabel *label;
@property (nonatomic, assign)int repeatTime;
//第一、二種的屬性
//@property (nonatomic, strong)NSTimer *timer;
//第三種方法的屬性
@property (nonatomic, strong)WLZTimer *timer;

@end

@implementation WLZSecondController


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor yellowColor];
    self.repeatTime = 60;
    self.label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
    self.label.text = @"60";
    self.label.textColor = [UIColor blackColor];
    [self.view addSubview:self.label];
    [self createTimer];
}

#pragma mark - 第一種方法
- (void)createTimer{
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(change) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if(parent == nil){
        [self.timer invalidate];
    }
}

- (void)change{
    self.repeatTime --;
    self.label.text = [NSString stringWithFormat:@"%d",self.repeatTime];
    if(self.repeatTime == 0){
        [self.timer invalidate];
    }
}

- (void)dealloc{
    NSLog(@"dealloc");
}
複製程式碼

二、自定義返回按鈕銷燬NSTimer

這種方法就需要禁止掉側滑返回手勢。

#pragma mark - 第二種方法  這裡只是隨意建立了一個button,具體的圖片、文案可以自己除錯。
- (void)createTimer{
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(change) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)viewWillAppear:(BOOL)animated{
    [self changeBackBarButtonItem];
}

- (void)changeBackBarButtonItem{
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(invalidateTimer)];
}

- (void)invalidateTimer{
    [self.timer invalidate];
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)change{
    self.repeatTime --;
    self.label.text = [NSString stringWithFormat:@"%d",self.repeatTime];
    if(self.repeatTime == 0){
        [self.timer invalidate];
    }
}

- (void)dealloc{
    NSLog(@"dealloc");
}
複製程式碼

三、自定義timer,壓根不形成迴圈引用

自定義timer類

.h檔案
@interface WLZTimer : NSObject

@property (nonatomic, weak)id target;
@property (nonatomic, assign)SEL selector;

///建立timer
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval Target:(id)target andSelector:(SEL)selector;
///銷燬timer
- (void)closeTimer;

@end

.m檔案
@interface WLZTimer ()

@property (nonatomic, strong)NSTimer *timer;

@end

@implementation WLZTimer

- (instancetype)initWithTimeInterval:(NSTimeInterval)interval Target:(id)target andSelector:(SEL)selector{
    if(self == [super init]){
        self.target = target;
        self.selector = selector;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(dosomething) userInfo:nil repeats:YES];
    }
    return self;
}

- (void)dosomething{
    //這裡是為了不阻塞主執行緒
    dispatch_async(dispatch_get_main_queue(), ^{
        id target = self.target;
        SEL selector = self.selector;
        if([target respondsToSelector:selector]){
            [target performSelector:selector withObject:nil];
        }
    });
}

- (void)closeTimer{
    [self.timer invalidate];
    self.timer = nil;
}

- (void)dealloc{
    NSLog(@"WLZTimer dealloc");
}

@end
複製程式碼

自定義timer在主執行緒非同步執行任務不明白原因的話,可參考文章iOS執行緒、同步非同步、序列並行佇列

建立timer的時候就用自定義的類就可以

#pragma mark - 第三種方法
/*
 *  與第前兩種不同的是:前兩種只是在合適的時機解決迴圈引用,
 *  第三種根本就不會造成迴圈引用,可以封裝起來供多個地方使用,而且遵循單一職責原則
 *
 */
- (void)createTimer{
    self.timer = [[WLZTimer alloc] initWithTimeInterval:1 Target:self andSelector:@selector(change)];
}

- (void)change{
    self.repeatTime --;
    self.label.text = [NSString stringWithFormat:@"%d",self.repeatTime];
    if(self.repeatTime == 0){
        [self.timer closeTimer];
    }
}

- (void)dealloc{
    [self.timer closeTimer];
    NSLog(@"dealloc");
}
複製程式碼

2019/1/17 更新

四、定義中介繼承NSObject進行訊息轉發消除強引用NSTimer

+ (instancetype)proxyWithTarget:(id)aTarget{
    TFQProxy *proxy = [[TFQProxy alloc] init];
    proxy.target = aTarget;
    return proxy;
}

//自己不能處理這個訊息,就會呼叫這個方法來訊息轉發,return target,讓target來呼叫這個方法。
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if([self.target respondsToSelector:aSelector]){
        return self.target;
    }
    return nil;
}
複製程式碼
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TFQProxy proxyWithTarget:self] selector:@selector(repeatAction:) userInfo:nil repeats:YES];
複製程式碼

五、定義中介繼承NSProxy進行訊息轉發消除強引用NSTimer

+ (instancetype)proxyWithTarget:(id)aTarget{
    TFQProxySubclass *proxy = [TFQProxySubclass alloc];
    proxy.target = aTarget;
    return proxy;
}

//為另一個類實現的訊息建立一個有效的方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

//將選擇器轉發給一個真正實現了該訊息的物件
- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
複製程式碼
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TFQProxySubclass proxyWithTarget:self] selector:@selector(repeatAction:) userInfo:nil repeats:YES];
複製程式碼

tips:五比四效率高,因為5是系統的類,直接進行訊息轉發,4會走幾條彎路才會到訊息轉發那個方法

六、GCD建立定時器

/**
 GCD建立定時器

 @param task 定時器內容
 @param interval 執行間隔
 @param repeat 是否重複
 @param async 是否非同步
 @param identifier 定時器唯一ID
 @return 返回定時器唯一ID,銷燬的時候用
 */
+ (NSString *)schedleTask:(void (^)(void))task interval:(NSTimeInterval)interval repeat:(BOOL)repeat async:(BOOL)async reuseIdentifier:(NSString *)identifier{
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    //穿件定時器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //開始時間
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 0*NSEC_PER_SEC);
    //設定各種時間
    dispatch_source_set_timer(timer, start, interval*NSEC_PER_SEC, 0);
    //設定回撥
    dispatch_source_set_event_handler(timer, ^{
        task();
        if(!repeat){
            [self cancelTimer:identifier];
        }
    });
    //啟動定時器
    dispatch_resume(timer);
    timerDict[identifier] = timer;
    return identifier;
}

+ (void)cancelTimer:(NSString *)identifier{
    dispatch_source_cancel(timerDict[identifier]);
    [timerDict removeObjectForKey:identifier];
}
複製程式碼
__weak typeof(self) weakSelf = self;
self.timerIdentifier = [TFQGCDTimer schedleTask:^{
    [weakSelf repeatAction:nil];
} interval:1 repeat:YES async:NO reuseIdentifier:@"identifier"];
複製程式碼

相關文章