iOS定時器循環引用分析及完美解決方案

目錄

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,這樣沒有相互強引用,則不會造成循環引用。

打破相互強引用.png
  • 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

點個贊再走唄~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容