目錄
1.NSTimer導致的循環引用分析
2.NSTimer循環引用解決思路誤區
3.NSTimer循環引用解決方案
4.NSTimer不準確的問題探究及解決
1.NSTimer導致的循環引用分析
- CADisplayLink(頻率能達到屏幕刷新率的定時器類)也和NSTimer一樣會有此問題,這里為了方便只使用NSTimer去講解。
如下面代碼 在控制器創建一個NSTimer定時器:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
當我們退出控制器會發現dealloc沒有被調用,timerTest還是一直在執行,即明顯的發生了內存泄漏,原因如下:
- timer是ViewController的成員,即ViewController對timer是強引用;
-
NSTimer創建會對target傳入對象產生強引用,此時我們傳入了self(即是ViewController),即NSTimer對self也是強引用,關系如下圖
截屏2021-02-25 下午7.33.01.png
所以他們兩者相互被強引用,即發生了循環引用,dealloc 永遠不會被執行,timer 也永遠不會被釋放,造成內存泄漏。
2.NSTimer循環引用解決思路誤區
當我們遇到循環引用大多數第一反應就是使用weak解決,我們來看一下
- 一:把timer改成弱引用讓self對timer是弱引用
@property (weak, nonatomic) NSTimer *timer;
雖然self對timer是弱引用,但是timer銷毀在dealloc方法里面執行,而dealloc又需要timer銷毀才能執行,即兩者相互等待,循環引用問題依舊存在。
- 二:使用__weak self,讓timer弱引用self
weak關鍵字適用于block,當block引用了塊外的變量時,會根據修飾變量的關鍵字來決定是強引用還是弱引用,如果變量使用weak關鍵字修飾,那block會對變量進行弱引用,如果沒有__weak關鍵字,那就是強引用。
??但是NSTimer的 scheduledTimerWithTimeInterval:target方法內部不會判斷修飾target的關鍵字,所以這里傳self 和 weakSelf是沒區別的,其內部會對target進行強引用,還是會產生循環引用。
- 三 : 在“適當的時機釋放定時器”
比如在頁面即將消失的時候:
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
在某些情況下,這種做法是可以解決問題的,但是有時卻會引起其他問題,比如控制器push到下一個控制器,viewDidDisappear執行后,timer被釋放,此時再pop回來,timer已經不復存在了。
所以,這種"方案"并不是合理的。
??優化上面的方法這個時候可以采用配對使用在 viewWillAppear 開timer啟,在 viewWillDisappear 關閉timer:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if (!self.timer) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
雖然能解決問題,但是在項目中,每次使用定時器都寫這么一堆,顯得不夠優雅,維護起來也比較麻煩。而且有的業務場景這么使用依舊會產生各種問題。
NSTimer循環引用解決方案:
1、使用block的創建方式解決:
iOS10中,NSTimer的API新增了block方法,我們可以直接使用這種方式創建定時器結合weak使用即可
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
2、給self添加中間對象TimerPoxy
??引入一個中間對象TimerPoxy,TimerPoxy弱引用 self,然后 TimerPoxy 傳入NSTimer。即self 強引用NSTimer,NSTimer強引用 TimerPoxy,TimerPoxy 弱引用 self,這樣沒有相互強引用,則不會造成循環引用。
- TimerPoxy的實現:
TimerProxy.h
#import <Foundation/Foundation.h>
@interface TimerProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
TimerProxy.m
#import "TimerProxy.h"
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target
{
TimerProxy *proxy = [[TimerProxy alloc] init];
proxy.target = target;
return proxy;
}
/*
因為這里并沒有實現定時器的timerTest方法,
timerTest方法是在ViewController里面實現的,
所以這里我們要利用消息轉發機制,把方法交給ViewController去實現
**/
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
??消息轉發,簡單來說就是如果當前對象沒有實現這個方法,系統會到這個方法里來找實現對象。
??本文中由于當前target是TimerProxy,但是TimerProxy沒有實現timerTest方法(當然也不需要它實現),讓系統去找target實例的方法實現,也就是去找ViewController中的方法實現。
- ViewController中的實現:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:[TimerProxy proxyWithTarget:self]
selector:@selector(timerTest)
userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
這樣就實現上面圖示的內容,解決了循環引用的問題。
優化解決方案:
- 不使用NSObject類實現,使用NSProxy類去實現。
上面我們用NSObject實現轉發定時器的過程大概是:
當前類查找timerTest實現方法 ---> NSObject 類找timerTest實現方法 --->消息轉發至ViewController查找實現方法。
而NSProxy一般就是用來做消息轉發的,大概過程:
直接把實現方法轉發ViewController。
這樣對比下來,NSProxy比NSObject效率高且節省資源
NSProxy實現:
#import <Foundation/Foundation.h>
@interface TimerProxys : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "TimerProxys.h"
@implementation TimerProxys
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy對象不需要調用init,因為它本來就沒有init方法
TimerProxys *proxy = [TimerProxys alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
到此定時器的循環引用問題,已經完美解決啦~
4.NSTimer不準確的問題探究及解決
為什么NSTimer會不準確?
1、NSTimer定時器依賴于RunLoop,我們知道RunLoop每循環一次的時間是基于任務量的,即每一次的時間都不一定相同,如果RunLoop的任務過于繁重,可能會導致NSTimer不準時。
2、模式的切換,當創建的timer被加入到NSDefaultRunLoopMode時,此時如果有滑動UIScrollView的操作,runLoop 的mode會切換為TrackingRunLoopMode,而MainRunLoop處于 UITrackingRunLoopMode 的模式下,是不會處理 NSDefaultRunLoopMode 的消息(因為它們的RunLoop Mode不一樣),所以timer會暫時停止;
對于精度要求不高的場景我們使用NSTimer沒太大影響,對于上面2的滑動UIScrollView的導致定時器停止的問題下面一行代碼即可解決:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- 解決方案:
使用GCD定時器,因為GCD定時器是基于系統內核的,所以不會受其他因素影響,基本使用方法這里不作講解,github有相關封裝demon,有需要自取。
本文代碼demon地址:https://github.com/yizhixiafancai/timerAbout
點個贊再走唄~