iOS中关于Timer的使用须知

NSTimer的使用问题

NSTimer做计时器循环事件的时候,很有可能会遇到以下两个问题:

  1. 正常启动的timer在滚动视图滚动的时候不能够接收事件消息了
  2. 当前引用timer的类不能够得到释放,进而造成内存泄露的问题

所以针对于以上问题,进行记录与说明。

产生原因以及解决方法

正常启动的timer在滚动视图滚动的时候不能够接收事件消息了

因为系统的timer记时器是通过iOS中的Runloop实现的,每一个定时器timer的实例都需要加入到Runloop中才能够有效,由于Runloop有五种模式,分别是NSDefaultRunLoopMode、NSEventTrackingRunLoopMode、NSModalPaneRunLoopMode、NSTrackingRunLoopMode、NSRunLoopCommonModes

RunloopModes

这五种模式会在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被当前的对象引用,而启动定时器的时候,又将当前对象作为参数传入到定时器中,二者相互引用导致循环引用的产生。如下图:

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来解除相互引入问题

引入新对象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();}
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容