美文网首页
iOS中关于Timer的使用须知

iOS中关于Timer的使用须知

作者: a_只羊 | 来源:发表于2020-12-01 17:10 被阅读0次

    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();}
    }
    

    相关文章

      网友评论

          本文标题:iOS中关于Timer的使用须知

          本文链接:https://www.haomeiwen.com/subject/hvrswktx.html