iOS中關於Timer的使用須知

NSTimer的使用問題

NSTimer做計時器循環事件的時候,很有可能會遇到以下兩個問題:

  1. 正常啓動的timer在滾動視圖滾動的時候不能夠接收事件消息了
  2. 當前引用timer的類不能夠得到釋放,進而造成內存泄露的問題

所以針對於以上問題,進行記錄與說明。

產生原因以及解決方法

正常啓動的timer在滾動視圖滾動的時候不能夠接收事件消息了

因爲系統的timer記時器是通過iOS中的Runloop實現的,每一個定時器timer的實例都需要加入到Runloop中才能夠有效,由於Runloop有五種模式,分別是NSDefaultRunLoopMode、NSEventTrackingRunLoopMode、NSModalPaneRunLoopMode、NSTrackingRunLoopMode、NSRunLoopCommonModes

這五種模式會在Runloop的不同的場景下進行來回切換,而定時器timer如果沒有加入到切換對應的場景mode中,則就會導致當前的mode中不存在加入的timer,也就會引發timer接收不到定時器消息的問題。本質是runloop因爲切換mode,且對應mode中沒有當前的timer對象,在當前的mode中,導致timer收不到事件消息的問題。

解決方法其實很簡單,在創建定時器的時候,將定時器加入到runloop的不同的mode中,這樣就能確保runloop在切換mode的時候能夠找到對應mode中的定時器,也就能夠發送定時器消息以保證定時器回調事件的正常了。

//注意,以下的方法會導致循環引用的發生,直接導致timer釋放不掉,解決方案在第二個問題記錄中
- (void)normalTimer{
    self.timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)cycleTimer{
    self.timer =
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
}

- (void)timerEvent{
    NSLog(@"timer事件--%s",__func__);
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}

當前引用timer的類不能夠得到釋放,進而造成內存泄露的問題

以上的定時器timer雖然能夠在Runloop的各種mode中完美運行,但是會導致當前的對象與timer相互引用導致循環引用問題的產生。總結來說就是:
由於定時器timer被當前的對象引用,而啓動定時器的時候,又將當前對象作爲參數傳入到定時器中,二者相互引用導致循環引用的產生。如下圖:

這裏說一種錯誤的解決方法:將self改成weak類型後依舊會有循環引用,原因是修改weak屬性只對block有效,對於timer對象的內部Targetstrong引用是沒有效果的。

本質是循環引用導致的內存泄露,所以在相互引用上解除引用纔是解決的根本。這裏有兩種方案去解決這樣的問題:

  1. 如果是iOS10以上,我們可以直接使用timerscheduledTimerWithTimeInterval:repeats:block:方法進行設置
- (void)timerBlock{
    if (__builtin_available(iOS 10, *)) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"%s",__func__);
        }];
    } else {}
}
  1. 可以引入新的對象C,將引用鏈由A->B,B->A 改成 A->C, C->B, B->A

引入新對象C之後,三者引用關係就如上圖,這樣就不存在兩個對象之間相互引用了,在銷燬對象的時候,只需要消除其中一條引用,則可以全部消除引用關係。比如ObjectA在銷燬前,可以向Timer發送invalidate消息,消除對於ObjectC的引用,這樣就消除了一個引用關係,過程如下:

  1. 調用timerinvidate方法結束定時器對對象C的引用,讓引入的新對象Cdealloc
  2. 引入的新對象C的釋放,結束了對於對象A的引用,當前對象A也緊接着dealloc
  3. 當前對象A的釋放,結束了對於定時器B的引用,定時器對象B也緊接着dealloc了

基於上述的問題,我們可以封裝一個解除timer引用的臨時對象,對象的內容實現如下:

LCSafeObj.h

//
//  LCSafeObj.h
//  Timer
//
//  Created by Leo on 2020/12/1.
//
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LCSafeObj : NSObject

+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat obj:(id)object;

@end

NS_ASSUME_NONNULL_END


LCSafeObj.m

//
//  LCSafeObj.m
//  Timer
//
//  Created by Leo on 2020/12/1.
//

#import "LCSafeObj.h"

@interface LCSafeObj ()

@property (nonatomic, strong) id target;
@property (nonatomic, assign) SEL selecter;

@end

@implementation LCSafeObj

- (instancetype)initWithTarget:(id)target selecter:(SEL)selecter{
    if (self = [super init]) {
        self.target = target;
        self.selecter = selecter;
    }
    return self;
}

+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat obj:(id)object{
   //此時LCSafeObj單獨引用外部對象
    LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter];
    //注意這裏的Target傳入的是LCSafeObj類型的,並不是外部對象,目的是讓定時器timer引用新引入的對象C,
    NSTimer *timer =
    [NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:selecter userInfo:object repeats:repeat];
    //返回給傳入的對象,讓其單引用定時器timer,且控制定時器的invalid的時間,至此完成單鏈的引用
    return timer;
}


/// 使用消息轉發來將SafeObj中沒有的方法調用轉移到傳入的對象中
/// @param aSelector 方法轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == self.selecter) {
        return self.target;
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}

@end

這裏實現的過程中注意用到了運行時的消息轉發機制,以確保傳入對象的正確方法調用,以及代碼的簡潔。

優化內容

上述的方法存在一些瑕疵,就是使用的時候可能還是需要在當前使用的類中去手動invalidDate timer計時器才能夠將三者釋放掉,這樣在開發的過程中也是比較繁瑣的,可以考慮將釋放工作放到引入的三方對象C中,具體做法參考如下:

//
//  LCSafeObj.m
//  Timer
//
//  Created by Leo on 2020/12/1.
//

#import "LCSafeObj.h"

@interface LCSafeObj ()

@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selecter;
@property (nonatomic, copy) void (^timerEventBlock)(void);

@end

@implementation LCSafeObj

- (instancetype)initWithTarget:(id)target selecter:(SEL)selecter timerEventBlock:(void (^)(void))timerEventBlock{
    
    if (self = [super init]) {
        self.target = target;
        self.selecter = selecter;
        self.timerEventBlock = timerEventBlock;
    }
    return self;
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}

- (void)setTimer:(NSTimer *)timer{
    _timer = timer;
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat{
    //此時LCSafeObj單獨引用外部對象
    LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter timerEventBlock:nil];
    //注意這裏的Target傳入的是LCSafeObj類型的,並不是外部對象,目的是讓定時器timer引用新引入的對象C,
    safeObj.timer =
    [NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
    //返回給傳入的對象,讓其單引用定時器timer,且控制定時器的invalid的時間,至此完成單鏈的引用
    return safeObj.timer;
}

- (void)targetAction{
    if (!self.target) {
        [self.timer invalidate];
    }
    if (self.target && [self.target respondsToSelector:self.selecter]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selecter];
#pragma clang diagnostic pop
    }
    if (self.timerEventBlock) {self.timerEventBlock();}
}

+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval isRepeat:(BOOL)repeat eventBlock:(void (^)(void))eventBlock{
    //此時LCSafeObj單獨引用外部對象
    LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:nil selecter:nil timerEventBlock:eventBlock];
    //注意這裏的Target傳入的是LCSafeObj類型的,並不是外部對象,目的是讓定時器timer引用新引入的對象C,
    safeObj.timer =
    [NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
    //返回給傳入的對象,讓其單引用定時器timer,且控制定時器的invalid的時間,至此完成單鏈的引用
    return safeObj.timer;
}


@end

優化方案兩個要點:

  1. 對外部target的引用採取weak弱引用,以保證外部對象的正常釋放
@property (nonatomic, weak) id target;
  1. 定時器事件方法中判斷target引用是否依舊存在,不存在則使用invalidDate 去除定時器timer對於引入的對象C的引用
- (void)targetAction{
    if (!self.target) {
        [self.timer invalidate];
    }
    if (self.target && [self.target respondsToSelector:self.selecter]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selecter];
#pragma clang diagnostic pop
    }
    if (self.timerEventBlock) {self.timerEventBlock();}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章