iOS之NSTimer迴圈引用的解決方案

前言

在使用NSTimer,如果使用不得當特別會引起迴圈引用,造成記憶體洩露。所以怎麼避免迴圈引用問題,下面我提出幾種解決NSTimer的幾種迴圈引用。

原因

當你在ViewController(簡稱VC)中使用timer屬性,由於VC強引用timer,timer的target又是VC造成迴圈引用。當你在VC的dealloc方法中銷燬timer,

發現VC被pop,VC的dealloc方法沒走,VC在等timer釋放才走dealloc,timer釋放在dealloc中,所以引起迴圈引用。

解決方案

在ViewController執行dealloc前釋放timer(不推薦)

對定時器NSTimer封裝

蘋果API介面解決方案(iOS 10。0以上可用)

使用block進行解決

使用NSProxy進行解決

一、在ViewController執行dealloc前釋放timer(不推薦)

可以在viewWillAppear中建立timer

可以在viewWillDisappear中銷燬timer

二、對定時器NSTimer封裝到PFTimer中

程式碼如下:

//PFTimer。h檔案#import @interface PFTimer : NSObject//開啟定時器- (void)startTimer;//暫停定時器- (void)stopTimer;@end複製程式碼

在PFTimer。m檔案中程式碼如下:

#import “PFTimer。h”@implementation PFTimer { NSTimer *_timer;}- (void)stopTimer{ if (_timer == nil) { return; } [_timer invalidate]; _timer = nil;}- (void)startTimer{ _timer = [NSTimer scheduledTimerWithTimeInterval:1。0 target:self selector:@selector(work) userInfo:nil repeats:YES];}- (void)work{ NSLog(@“正在計時中……”);}- (void)dealloc{ NSLog(@“%s”,__func__); [_timer invalidate]; _timer = nil;}@end複製程式碼

在ViewController中使用程式碼如下:

#import “ViewController1。h”#import “PFTimer。h”@interface ViewController1 ()@property (nonatomic, strong) PFTimer *timer;@end@implementation ViewController1- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated];}- (void)viewDidLoad { [super viewDidLoad]; self。title = @“VC1”; self。view。backgroundColor = [UIColor whiteColor]; //自定義timer PFTimer *timer = [[PFTimer alloc] init]; self。timer = timer; [timer startTimer];}- (void)dealloc { [self。timer stopTimer]; NSLog(@“%s”,__func__);}複製程式碼

執行列印結果:

-[ViewController1 dealloc]-[PFTimer dealloc]複製程式碼

這個方式主要就是讓PFTimer強引用NSTimer,NSTimer強引用PFTimer,避免讓NSTimer強引用ViewController,這樣就不會引起迴圈引用,然後在dealloc方法中執行NSTimer的銷燬,相對的PFTimer也會進行銷燬了。

三、蘋果系統API可以解決(iOS10以上)

在iOS 10。0以後,蘋果官方新增了關於NSTimer的三個API:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10。12), ios(10。0), watchos(3。0), tvos(10。0));+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10。12), ios(10。0), watchos(3。0), tvos(10。0));- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10。12), ios(10。0), watchos(3。0), tvos(10。0));複製程式碼

這三個方法都有一個Block的回撥方法。關於block引數,官方文件有說明:

the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references。複製程式碼

翻譯過來就是說,定時器在執行時,將自身作為引數傳遞給block,來幫助避免迴圈引用。使用很簡單,但是要注意兩點:

1。避免block的迴圈引用,使用__weak和__strong來避免

2。在持用NSTimer物件的類的方法中-(void)dealloc呼叫NSTimer 的- (void)invalidate方法;

四、使用block來解決

透過建立一個NSTimer的category名字為PFSafeTimer,在NSTimer+PFSafeTimer。h程式碼如下:

#import NS_ASSUME_NONNULL_BEGIN@interface NSTimer (PFSafeTimer)+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats;@endNS_ASSUME_NONNULL_END複製程式碼

在NSTimer+PFSafeTimer。m中的程式碼如下:

#import “NSTimer+PFSafeTimer。h”@implementation NSTimer (PFSafeTimer)+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats { return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(handle:) userInfo:[block copy] repeats:repeats];}+ (void)handle:(NSTimer *)timer { void(^block)(void) = timer。userInfo; if (block) { block(); }}@end複製程式碼

該方案主要要點:

將計時器所應執行的任務封裝成“Block”,在呼叫計時器函式時,把block作為userInfo引數傳進去。

userInfo引數用來存放“不透明值”,只要計時器有效,就會一直保留它。

在傳入引數時要透過copy方法,將block複製到“堆區”,否則等到稍後要執行它的時候,該blcok可能已經無效了。

計時器現在的target是NSTimer類物件,這是個單例,因此計時器是否會保留它,其實都無所謂。此處依然有保留環,然而因為類物件(class object)無需回收,所以不用擔心。

再呼叫如下:

#import “ViewController1。h”#import “PFTimer。h”#import “NSTimer+PFSafeTimer。h”@interface ViewController1 ()//使用category@property (nonatomic, strong) NSTimer *timer1;@end@implementation ViewController1- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated];}- (void)viewDidLoad { [super viewDidLoad]; self。title = @“VC1”; self。view。backgroundColor = [UIColor whiteColor]; __weak typeof(self) weakSelf = self; self。timer1 = [NSTimer PF_ScheduledTimerWithTimeInterval:1。0 block:^{ __strong typeof(self) strongSelf = weakSelf; [strongSelf timerHandle]; } repeats:YES];}//定時觸發的事件- (void)timerHandle { NSLog(@“正在計時中……”);}- (void)dealloc { // [self。timer stopTimer]; NSLog(@“%s”,__func__);}複製程式碼

如果在block裡面直接呼叫self,還是會保留環的。因為block對self強引用,self對timer強引用,timer又透過userInfo引數保留block(強引用block),這樣就構成一個環block->self->timer->userinfo->block,所以要打破這個環的話要在block裡面弱引用self。

使用NSProxy來解決迴圈引用

原理如下圖:

iOS之NSTimer迴圈引用的解決方案

NSProxy解決迴圈引用原理。png

程式碼如下:

//PFProxy。h#import NS_ASSUME_NONNULL_BEGIN@interface PFProxy : NSProxy//透過建立物件- (instancetype)initWithObjc:(id)object;//透過類方法建立建立+ (instancetype)proxyWithObjc:(id)object;@endNS_ASSUME_NONNULL_END複製程式碼

在PFProxy。m檔案中寫程式碼

#import “PFProxy。h”@interface PFProxy()@property (nonatomic, weak) id object;@end@implementation PFProxy- (instancetype)initWithObjc:(id)object { self。object = object; return self;}+ (instancetype)proxyWithObjc:(id)object { return [[self alloc] initWithObjc:object];}- (void)forwardInvocation:(NSInvocation *)invocation { if ([self。object respondsToSelector:invocation。selector]) { [invocation invokeWithTarget:self。object]; }}- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self。object methodSignatureForSelector:sel];}@end複製程式碼

在使用的時候如下程式碼:

#import “ViewController1。h”#import “PFProxy。h”@interface ViewController1 ()//使用NSProxy@property (nonatomic, strong) NSTimer *timer2;@end@implementation ViewController1- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated];}- (void)viewDidLoad { [super viewDidLoad]; self。title = @“VC1”; self。view。backgroundColor = [UIColor whiteColor]; PFProxy *proxy = [[PFProxy alloc] initWithObjc:self]; self。timer2 = [NSTimer scheduledTimerWithTimeInterval:1。0 target:proxy selector:@selector(timerHandle) userInfo:nil repeats:YES];}//定時觸發的事件- (void)timerHandle { NSLog(@“正在計時中……”);}- (void)dealloc { [self。timer2 invalidate]; self。timer2 = nil; NSLog(@“%s”,__func__);}@end複製程式碼

當pop當前viewController時候,列印結果:

-[ViewController1 dealloc]複製程式碼

透過PFProxy這個偽基類(相當於ViewController1的複製類),避免直接讓timer和viewController造成迴圈。

作者:hello小李

連結:https://juejin。im/post/5da72cb6e51d4524d55484cf

著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。