iOS 最詳細清晰的NSTimer定時器與內存泄漏剖析

NStimer的基本用法

NSTimer有如下兩種基本的使用方式:
1. 創建對象並加入到當前的runloop裏

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

2. 直接使用類方法創建並啓動定時器

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"--->> NSTimer block.");
}];

第二種使用方法本質上和第一種是一樣的,此方法內部完成了把創建的定時器對象加入runloop的工作。

內存泄漏

分析如上的用法,可以發現,假設是在一個控制器裏使用上述定時器,控制器持有了定時器對象,定時器對象又會通過對target對象(此時爲控制器)的引用,造成循環引用導致內存泄漏。下圖紅色部分即爲循環引用。
在這裏插入圖片描述

解除循環引用

無效的解除方法
基於iOS ARC的內存管理模式,在某對象引用計數爲0的時候,系統框架會通知對象調用其析構函數dealloc。
但是我們分析上述的循環引用可知,直接在控制器的析構函數調用定時器的失效方法是無法解除循環引用的,因爲控制器的引用計數在被定時器引用的情況下,不可能爲0,所以dealloc方法不會被調用,也就無法使定時器失效。

//直接在析構函數使定時器失效的方法永遠不會被調用
- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"SZTimerViewController dealloc.");
}

在適當的時機主動解除環的某一方的引用
控制器的生命週期方法didMoveToParentViewController,在控制器添加到父控制器和從父控制器移除時都會被系統框架通知控制器調用,所以可以重寫此方法,判斷在父控制器參數爲nil時(控制器從父控制器移除),將定時器失效。

- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self.timer invalidate];//調用此方法,定時器解除對target的引用和runloop解除對定時器的引用;
        self.timer = nil;//不置nil,偶爾會有timer沒釋放的情況
    }
}

當退出當前頁面時,強引用的解除順序是:

  1. 在控制器移除時,父控制器解除對當前頁面控制器的引用;
  2. didMoveToParentViewController被調用,定時器解除對當前頁面控制器(target)的引用和runloop解除對定時器的引用;
  3. 當前頁面控制器的引用計數爲0,銷燬當前頁面控制器,當前頁面控制器銷燬後則解除了對定時器的引用,定時器的引用計數爲0,則銷燬定時器。

效果如下圖所示。
在這裏插入圖片描述
注意:不能在控制器的其他生命週期方法如,viewDidDisappear:方法等來做定時器失效的工作,只能是在控制器移除的方法裏做,因爲其他方法如viewDidDisappear,當從當前控制器頁面進入下一級頁面時,一般不希望定時器失效。

使用runtime運行時引入第三方對象作爲定時器的target
原理是,換一個target對象,不與定時器的持有者形成環引用,運用runtime運行時將定時器處理方法添加到新的target對象,將處理方法發送到新的target處理。
對象之間不再有循環引用,則控制器的析構函數可以被調用,在其析構函數調用定時器失效方法即可。引用關係如下圖所示。
在這裏插入圖片描述
實現方法如下:

//    2.1 換一個timer的target,不與self形成環引用,並使用運行時結束爲新的target新增定時器的定時處理方法
self.timerTarget = [NSObject new];
class_addMethod([self.timerTarget class], @selector(fire), (IMP)fireImp, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.timerTarget selector:@selector(fire) userInfo:nil repeats:YES];
//2.2 定時器定時要處理的工作
void fireImp(id self, SEL _cmd) {
    NSLog(@"--->> fireImp");
}
- (void)dealloc {
//    2.3在控制器銷燬前,使定時器失效
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"SZTimerViewController dealloc.");
}

強引用解除順序是:

  1. 在控制器移除時,父控制器解除對當前頁面控制器的引用,當前頁面控制器引用計數爲0;
  2. 當前頁面控制器調用析構函數,定時器解除對target的引用和runloop解除對定時器的引用;
  3. 當前頁面控制器的引用計數爲0,銷燬當前頁面控制器,當前頁面控制器銷燬後則解除了對target和定時器的引用,target和定時器的引用計數都爲0,則銷燬target和定時器。

效果如下圖所示。
在這裏插入圖片描述

使用消息轉發重定向引入第三方對象作爲定時器的target
思路本質上與上述第二種方式一樣,都是通過引入第三方對象做target,解除引用成環的問題。
與第二種方式思路略有不同的是,引入的第三方target會弱引用控制器對象,給第三方target的方法消息重定向至控制器時使用。
對象之間不再有循環引用,則控制器的析構函數可以被調用,在其析構函數調用定時器失效方法即可。引用關係如下圖。
在這裏插入圖片描述
實現方法如下:

//  3.1 新建proxyTarget類
@interface SZTimerProxyTarget : NSProxy
//消息重定向的處理對象
@property (nonatomic, weak) id target;
@end
//   3.2 將方法消息重定向給控制器處理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
//   3.3 使用proxy對象作爲target,將向proxy target發送的定時器處理消息,通過在proxy target裏進行消息轉發重定向,重定向至本定時器所在的控制器對象的方法來處理
self.proxyTarget = [SZTimerProxyTarget alloc];
self.proxyTarget.target = self;    
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxyTarget selector:@selector(fire) userInfo:nil repeats:YES];
//    3.4 在控制器銷燬前,使定時器失效
- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"SZTimerViewController dealloc.");
}

強引用解除順序是:

  1. 在控制器移除時,父控制器解除對當前頁面控制器的引用,當前頁面控制器引用計數爲0;
  2. 當前頁面控制器調用析構函數,定時器解除對proxyTarget的引用和runloop解除對定時器的引用;
  3. 當前頁面控制器的引用計數爲0,銷燬當前頁面控制器,當前頁面控制器銷燬後則解除了對target和定時器的引用,proxyTarget和定時器的引用計數都爲0,則銷燬proxyTarget和定時器。

效果如下圖所示。
在這裏插入圖片描述

發佈了50 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章