iOS小知識之NSTimer的迴圈引用一

語言: CN / TW / HK

小知識,大挑戰!本文正在參與“程式設計師必備小知識”創作活動。

1.NSTimer的迴圈引用

1.1 常見問題

日常開發中,經常會用到NSTimer定時器,一些不正確的寫法,會導致NSTimer造成迴圈引用,如下程式碼所示

``` - (void)viewDidLoad { [super viewDidLoad];

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

} - (void)fireHome{ num++; NSLog(@"hello word - %d",num); } - (void)dealloc{ [self.timer invalidate]; self.timer = nil; } `` 上述案例,一定會產生迴圈引用 - 建立NSTimer時,將self傳入target,導致NSTimer持有self,而self又持有timer- 使用NSTimerinvalidate方法,可以解除NSTimerself的持有 - 但是案例中,NSTimerinvalidate方法,由UIViewControllerdealloc方法執行。但是selftimer持有,只要timer有效,UIViewControllerdealloc`方法就不會執行。故此雙方相互等待,造成迴圈引用

1.2 target傳入弱引用物件

NSTimertarget引數傳入一個弱引用的self,能否打破對self的強持有? 程式碼如下:

``` - (void)viewDidLoad { [super viewDidLoad];

__weak typeof(self) weakSelf = self; 
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES]; 
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

} `` - 肯定是不行的,因為在timerWithTimeInterval內部,使用強引用物件接收target`引數,所以外部定義為弱引用物件沒有任何意義 這種方式類似於以下程式碼:

__weak typeof(self) weakSelf = self; typeof(self) strongSelf = weakSelf; 在官方文件中,對target引數進行了明確說明:

timer說明.png - target:定時器觸發時指定的訊息傳送到的物件。計時器維護對該物件的強引用,直到它(計時器)失效


和Block的區別: Block將捕獲到的弱引用物件,賦值給一個強引用的臨時變數,當Block執行完畢,臨時變數會自動銷燬,解除對外部變數的持有。

2.常規解決方案

2.1 更換API

使用攜帶Block的方法建立NSTimer,避免target的強持有

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

2.2 在適當時機呼叫invalidate

根據業務需求,可以將NSTimerinvalidate方法寫在viewWillDisappear方法中

``` - (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated];

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; 
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

} - (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated];

[self.timer invalidate];
self.timer = nil;

} `` 或者寫在didMoveTo ParentViewController`方法中

- (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)didMoveToParentViewController:(UIViewController *)parent{ if (parent == nil) { [self.timer invalidate]; self.timer = nil; } }