iOS中關(guān)于Timer的使用須知

NSTimer的使用問(wèn)題

NSTimer做計(jì)時(shí)器循環(huán)事件的時(shí)候,很有可能會(huì)遇到以下兩個(gè)問(wèn)題:

  1. 正常啟動(dòng)的timer在滾動(dòng)視圖滾動(dòng)的時(shí)候不能夠接收事件消息了
  2. 當(dāng)前引用timer的類不能夠得到釋放,進(jìn)而造成內(nèi)存泄露的問(wèn)題

所以針對(duì)于以上問(wèn)題,進(jìn)行記錄與說(shuō)明。

產(chǎn)生原因以及解決方法

正常啟動(dòng)的timer在滾動(dòng)視圖滾動(dòng)的時(shí)候不能夠接收事件消息了

因?yàn)橄到y(tǒng)的timer記時(shí)器是通過(guò)iOS中的Runloop實(shí)現(xiàn)的,每一個(gè)定時(shí)器timer的實(shí)例都需要加入到Runloop中才能夠有效,由于Runloop有五種模式,分別是NSDefaultRunLoopMode、NSEventTrackingRunLoopMode、NSModalPaneRunLoopMode、NSTrackingRunLoopMode、NSRunLoopCommonModes

RunloopModes

這五種模式會(huì)在Runloop的不同的場(chǎng)景下進(jìn)行來(lái)回切換,而定時(shí)器timer如果沒(méi)有加入到切換對(duì)應(yīng)的場(chǎng)景mode中,則就會(huì)導(dǎo)致當(dāng)前的mode中不存在加入的timer,也就會(huì)引發(fā)timer接收不到定時(shí)器消息的問(wèn)題。本質(zhì)是runloop因?yàn)榍袚Qmode,且對(duì)應(yīng)mode中沒(méi)有當(dāng)前的timer對(duì)象,在當(dāng)前的mode中,導(dǎo)致timer收不到事件消息的問(wèn)題。

解決方法其實(shí)很簡(jiǎn)單,在創(chuàng)建定時(shí)器的時(shí)候,將定時(shí)器加入到runloop的不同的mode中,這樣就能確保runloop在切換mode的時(shí)候能夠找到對(duì)應(yīng)mode中的定時(shí)器,也就能夠發(fā)送定時(shí)器消息以保證定時(shí)器回調(diào)事件的正常了。

//注意,以下的方法會(huì)導(dǎo)致循環(huán)引用的發(fā)生,直接導(dǎo)致timer釋放不掉,解決方案在第二個(gè)問(wèn)題記錄中
- (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__);
}

當(dāng)前引用timer的類不能夠得到釋放,進(jìn)而造成內(nèi)存泄露的問(wèn)題

以上的定時(shí)器timer雖然能夠在Runloop的各種mode中完美運(yùn)行,但是會(huì)導(dǎo)致當(dāng)前的對(duì)象與timer相互引用導(dǎo)致循環(huán)引用問(wèn)題的產(chǎn)生??偨Y(jié)來(lái)說(shuō)就是:
由于定時(shí)器timer被當(dāng)前的對(duì)象引用,而啟動(dòng)定時(shí)器的時(shí)候,又將當(dāng)前對(duì)象作為參數(shù)傳入到定時(shí)器中,二者相互引用導(dǎo)致循環(huán)引用的產(chǎn)生。如下圖:

timer的循環(huán)引用

這里說(shuō)一種錯(cuò)誤的解決方法:將self改成weak類型后依舊會(huì)有循環(huán)引用,原因是修改weak屬性只對(duì)block有效,對(duì)于timer對(duì)象的內(nèi)部Targetstrong引用是沒(méi)有效果的。

本質(zhì)是循環(huán)引用導(dǎo)致的內(nèi)存泄露,所以在相互引用上解除引用才是解決的根本。這里有兩種方案去解決這樣的問(wèn)題:

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

引入新對(duì)象C之后,三者引用關(guān)系就如上圖,這樣就不存在兩個(gè)對(duì)象之間相互引用了,在銷毀對(duì)象的時(shí)候,只需要消除其中一條引用,則可以全部消除引用關(guān)系。比如ObjectA在銷毀前,可以向Timer發(fā)送invalidate消息,消除對(duì)于ObjectC的引用,這樣就消除了一個(gè)引用關(guān)系,過(guò)程如下:

  1. 調(diào)用timerinvidate方法結(jié)束定時(shí)器對(duì)對(duì)象C的引用,讓引入的新對(duì)象Cdealloc
  2. 引入的新對(duì)象C的釋放,結(jié)束了對(duì)于對(duì)象A的引用,當(dāng)前對(duì)象A也緊接著dealloc
  3. 當(dāng)前對(duì)象A的釋放,結(jié)束了對(duì)于定時(shí)器B的引用,定時(shí)器對(duì)象B也緊接著dealloc了

基于上述的問(wèn)題,我們可以封裝一個(gè)解除timer引用的臨時(shí)對(duì)象,對(duì)象的內(nèi)容實(shí)現(xiàn)如下:

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{
   //此時(shí)LCSafeObj單獨(dú)引用外部對(duì)象
    LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter];
    //注意這里的Target傳入的是LCSafeObj類型的,并不是外部對(duì)象,目的是讓定時(shí)器timer引用新引入的對(duì)象C,
    NSTimer *timer =
    [NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:selecter userInfo:object repeats:repeat];
    //返回給傳入的對(duì)象,讓其單引用定時(shí)器timer,且控制定時(shí)器的invalid的時(shí)間,至此完成單鏈的引用
    return timer;
}


/// 使用消息轉(zhuǎn)發(fā)來(lái)將SafeObj中沒(méi)有的方法調(diào)用轉(zhuǎn)移到傳入的對(duì)象中
/// @param aSelector 方法轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == self.selecter) {
        return self.target;
    }
    return [super forwardingTargetForSelector:aSelector];
}

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

@end

這里實(shí)現(xiàn)的過(guò)程中注意用到了運(yùn)行時(shí)的消息轉(zhuǎn)發(fā)機(jī)制,以確保傳入對(duì)象的正確方法調(diào)用,以及代碼的簡(jiǎn)潔。

優(yōu)化內(nèi)容

上述的方法存在一些瑕疵,就是使用的時(shí)候可能還是需要在當(dāng)前使用的類中去手動(dòng)invalidDate timer計(jì)時(shí)器才能夠?qū)⑷哚尫诺?,這樣在開(kāi)發(fā)的過(guò)程中也是比較繁瑣的,可以考慮將釋放工作放到引入的三方對(duì)象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{
    //此時(shí)LCSafeObj單獨(dú)引用外部對(duì)象
    LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter timerEventBlock:nil];
    //注意這里的Target傳入的是LCSafeObj類型的,并不是外部對(duì)象,目的是讓定時(shí)器timer引用新引入的對(duì)象C,
    safeObj.timer =
    [NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
    //返回給傳入的對(duì)象,讓其單引用定時(shí)器timer,且控制定時(shí)器的invalid的時(shí)間,至此完成單鏈的引用
    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{
    //此時(shí)LCSafeObj單獨(dú)引用外部對(duì)象
    LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:nil selecter:nil timerEventBlock:eventBlock];
    //注意這里的Target傳入的是LCSafeObj類型的,并不是外部對(duì)象,目的是讓定時(shí)器timer引用新引入的對(duì)象C,
    safeObj.timer =
    [NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
    //返回給傳入的對(duì)象,讓其單引用定時(shí)器timer,且控制定時(shí)器的invalid的時(shí)間,至此完成單鏈的引用
    return safeObj.timer;
}


@end

優(yōu)化方案兩個(gè)要點(diǎn):

  1. 對(duì)外部target的引用采取weak弱引用,以保證外部對(duì)象的正常釋放
@property (nonatomic, weak) id target;
  1. 定時(shí)器事件方法中判斷target引用是否依舊存在,不存在則使用invalidDate 去除定時(shí)器timer對(duì)于引入的對(duì)象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();}
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容