美文网首页iOSiOS面试题+基础知识
iOS NSTimer内存问题分析及解决方案

iOS NSTimer内存问题分析及解决方案

作者: huxinwen | 来源:发表于2019-04-30 15:53 被阅读19次

    NSTimer是我们经常使用的一个跟时间相关的oc类,作用是在指定的时间触发对应的事件。

    一、使用NSTimer

    在iOS10.0之前有:

    - (instancetype)initWithFireDate:(NSDate*)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullableid)ui repeats:(BOOL)rep;

    + (NSTimer*)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullableid)userInfo repeats:(BOOL)yesOrNo;

    + (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullableid)userInfo repeats:(BOOL)yesOrNo;

    我们试试 init构造方法:

    init构造方法

    结果发现日志竟然没有打印,what?

    试试timer开头的构造方法:

    timer构造方法

    结果发现日志同样没有打印,what?

    试试schedule开头的方法,会不会有奇迹出现:

    schedule构造方法

    奇迹出现了,what?Are you ok?好慌^_^,吓得我赶紧看了看苹果爸爸怎么说的。

    Timers work in conjunction with run loops. Run loops maintain strong references to their timers.Once scheduled on a run loop, the timer fires at the specified interval until it is invalidated. 

    苹果爸爸说

    大致的意思是timer是工作在runloop中,并被runloop强引用,只有加入runloop才能生效呢,然后苹果爸爸又说了,init跟timer开头的没有加入到任何runloop中,schedule开头的默认加入了当前的runloop中,所以就出现了上述的情况。

    好了赶快试试:

    试试

    果然就是这样的,苹果爸爸就是折腾人,干嘛不一样处理呢?俗话说,想灭之,必先予之,哦哦,玩笑,苹果爸爸肯定不是这样想,可能出于开发者的角度,更多的自主权。

    二、NSTimer准确么?

    答案是不准的,有两点需要说明:

    1、iOS7.0后,timer加入runloop后,并不是准确的在指定的时间点触发事件,是有一个容忍度的,大概在10%左右,苹果爸爸的原话如下:In iOS 7 and later and macOS 10.9 and later, you can specify a tolerance for a timer (tolerance). This flexibility in when a timer fires improves the system's ability to optimize for increased power savings and responsiveness. The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer doesn't fire before the scheduled fire date. For repeating timers, the next fire date is calculated from the original fire date regardless of tolerance applied at individual fire times, to avoid drift. The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of the tolerance property.

    As the user of the timer, you can determine the appropriate tolerance for a timer. A general rule, set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance has significant positive impact on the power usage of your application. The system may enforce a maximum value for the tolerance.

    2、runloop分5中mode,其中三种模式是开发者可以接触的,NSRunLoopCommonModesNSDefaultRunLoopModeUITrackingRunLoopMode。timer加入某种mode,只有会在对应的mode中触发事件,如果加入NSDefaultRunLoopMode,那么当runloop切换到UITrackingRunLoopMode(例如滑动UIScrollView)下,那么timer就会停止,直到切回NSDefaultRunLoopMode中,timer才会继续fire。

    所以说,如果需要精准的触发事件,那么建议通过GCD timer,因为CGD timer是通过内核port触发runloop的机制,就能够达到精准制导!

    三、关于内存泄露

    1、内存泄漏分析:

    什么,what?这个东西还有内存泄漏,不是ARC么?Are you ok?

    是的,没错,的确有内存泄漏的风险。上面苹果爸爸的文档里就说了,timer会被runloop强引用,直到被invalidate,他们关系是这样的:

    关系图

    根据上面的关系图,如果在target的dealloc方法里面invalidate掉timer,就会形成相互等待,造成循环引用,如下图:

    内存泄漏的写法

    2、内存泄漏解决方案:

    这简单,不再dealloc里面invalidate,在其他的时机去做这步,这样当然没问题,给你点个赞^ _ ^,但是有些需求我们就需要在整个生命周期内都需要维持timer,只能在dealloc里面走这步,怎么办?还是从关系图分析开始:

    权威认证通过的解决方案

    有些同学可能有疑问,既然是timer强引用target,我给他一个弱引用给他怎么样?是的,你想得有道理,但是,即使你传一个经过__weak修饰得self给timer,依旧然并卵,timer依旧会是强引用了target,不知道苹果爸爸内部对他做了什么手脚,已经哭晕在厕所,😄,就是下图的方式依旧不行:

    __weak没法打断强引用

    2.1、第三方作为target:

    其实构造方法里面还有一种方式没用:+ (NSTimer*)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation*)invocation repeats:(BOOL)yesOrNo;

    invocation构造方法

    亲测依旧没有打断强引用,但是给我们提供了一种思路,那就是第三方,oc有一个类开发基本上没用过,就是虚基类NSProxy:

    NSProxy

    大致意思就是用来转发消息用的。

    NSProxy

    子类需要重写两个方法:forwardInvocation: 和methodSignatureForSelector: 。具体用法参考NSProxy的简单使用,好了不多说,直接撸代码:

    TimerProxy TimerProxy TestViewController

    现在让我们看看类图结构:

    加入第三方,打断循环引用

    2.2、block打断:

    不多说,直接上代码:

    NSTimer分类.h NSTimer分类.m block验证

    其实呢苹果爸爸在iOS10.0以后,也意识到这个问题,这么简单的问题非得让开发者折腾,好吧,这个问题,我来解决,所以就有了iOS10.0的新版本API,新增了对应的block版本的构造方法:

    + (NSTimer*)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer*timer))blockAPI_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

    + (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer*timer))blockAPI_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

    - (instancetype)initWithFireDate:(NSDate*)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer*timer))blockAPI_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

    好了,就写到这了,NSTimer就这些,欢迎指正!!!

    相关文章

      网友评论

        本文标题:iOS NSTimer内存问题分析及解决方案

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