超级苦工
阅读 31
比较一下iOS中的三种定时器

NSTimer

NSTimer是iOS开发中的最常见的定时器。

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
复制代码
- (void)setupNSTimer {
    /// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimerAction) userInfo:nil repeats:YES];
    [timer fire];
}
复制代码

Timer不仅会持有target,也会持有userInfo对象。

还有使用block参数的接口:

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter:  ti    The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
复制代码

在iOS的Target-Action模式中, UIControl(如UIButton)对其target的持有方式是 weakRetained 的方式, 因此不会存在循环引用.

而NSTimer对其target持有的方式是 autorelease 方式, 即target会在其指定的runloop下一次执行时查看是否进行释放. 若repeats参数为YES, 则timer未释放情况下, target不会释放, 因而会引起循环引用; 若repeats参数设置为NO, 则target可以被释放而不会存在循环引用.

参考: iOS Target-Action模式下内存泄露问题深入探究

RunLoop

NSTimer是基于RunLoop的,以scheduledTimerWithTimeInterval:开头的方法会将NSTimer加到当前runloop的default mode上。

而以timerWithTimeInterval:开头的方法,则需要使用runloop的addTimer:方法,将其手动加到runloop上。

因此,这里通常有一个注意的点:即runloop的UITrackingMode下,定时器会失效。解决办法即将定时器加到runloop的commonModes上即可

NSTimer引发循环引用的本质是:

Current RunLoop -> CFRunLoopMode -> sources数组 -> __NSCFTimer -> _NSTimerBlockTarget -> self
复制代码

所以,必须保证NSTimer执行invalidate方法,self对象才能释放。

即使在UIViewController的dealloc方法手动添加NSTimer的销毁方法,也无法解除循环引用,因为该dealloc方法根本不会调用。

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

通常,在UIViewController中,可在关闭界面的时候手动销毁定时器,以解除循环引用。

循环引用

对于NSTimer,如何解除循环引用,通常有几种方式。

引入WeakContainer,弱持有target对象

WeakContainer对象弱引用self对象,然后Timer的target设置为WeakContainer对象,在WeakContainer对象中将消息转发给target来执行即可。

@interface WeakContainer : NSObject

- (instancetype)initWithTarget:(id)target;

@end


@interface WeakContainer ()

@property (nonatomic, weak) id target;

@end

@implementation WeakContainer

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

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.target respondsToSelector:aSelector]) {
        return self.target;
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"doesNotRecognizeSelector %@ %@", self.target, NSStringFromSelector(aSelector));
}

@end
复制代码
- (void)dealloc {
    [self removeTimer];
}

- (void)setupTimer {
    self.weakTimer = [NSTimer scheduledTimerWithTimeInterval:2
                                                      target:[[WeakContainer alloc] initWithTarget:self]
                                                    selector:@selector(onTimer)
                                                    userInfo:nil
                                                     repeats:YES];
    [self.weakTimer fire];
}

- (void)removeTimer {
  [self.weakTimer invalidate];
  self.weakTimer = nil;
}
复制代码

则,target对象的释放不再受到NSTimer的影响。

这里,使用了一个WeakContainer,继承自NSObject,对NSTimer的target进行弱持有。而更合适的方式,是使用NSProxy。

使用NSProxy抽象类
NSProxy implements the basic methods required of a root class, including those defined in the NSObjectProtocol protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation(_:) and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself
复制代码

NSProxy是除了NSObject之外的另一个基类,是一个抽象类,只能继承它,重写其消息转发的方法,将消息转发给另一个对象。

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
复制代码

除了重载消息转发机制的两个方法之外,NSProxy也没有其他功能了。即,使用NSProxy注定是用来转发消息的。

  1. NSProxy可以用来模拟多继承,proxy对象处理多个不同Class对象的消息。
  2. 继承自NSProxy的代理类会自动转发消息,而继承自NSObject的则不会,需要自行根据消息转发机制来进行处理。
  3. NSObject的Category中的方法不能转发
@interface WeakProxy : NSProxy

- (instancetype)initWithTarget:(id)target;

@end

@interface WeakProxy ()

@property (nonatomic, weak) id target;

@end

@implementation WeakProxy

- (instancetype)initWithTarget:(id)target {
    self = [WeakProxy alloc];
    self.target = target;
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

@end
复制代码
- (void)dealloc {
    [self removeTimer];
}

- (void)setupTimer {
    self.weakTimer = [NSTimer scheduledTimerWithTimeInterval:2
                                                      target:[[WeakProxy alloc] initWithTarget:self]
                                                    selector:@selector(onTimer)
                                                    userInfo:nil
                                                     repeats:YES];
    [self.weakTimer fire];
}

- (void)removeTimer {
  [self.weakTimer invalidate];
  self.weakTimer = nil;
}
复制代码

可以看出,两种方式的代码几乎相同。只是NSProxy的特点要仔细体会。

使用NSTimer的Category技巧
@interface NSTimer (WeakTimer)

+ (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                         repeats:(BOOL)yesOrNo
                                           block:(void(^)(void))block;

@end

@implementation NSTimer (WeakTimer)

+ (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                         repeats:(BOOL)yesOrNo
                                           block:(void(^)(void))block
{
    return [self scheduledTimerWithTimeInterval:ti
                                         target:self
                                       selector:@selector(onTimer:)
                                       userInfo:[block copy]
                                        repeats:yesOrNo];
}

+ (void)onTimer:(NSTimer *)timer {
    void (^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}

@end
复制代码
- (void)dealloc {
    [self removeTimer];
}

- (void)setupTimer {
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer weak_scheduledTimerWithTimeInterval:2 repeats:YES block:^{
        [weakSelf onTimer];
    }];
    [self.timer fire];
}

- (void)removeTimer {
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

这两种方式的实现不同,但本质上都要做到两点:

  1. NSTimer不能强持有self对象
  2. self对象的dealloc方法执行中,销毁NSTimer对象

CADisplayLink

CADisplayLink是以屏幕刷新频率将内容绘制到屏幕上的定时器,适合做UI的不停重绘,动画或视频的渲染等。

一旦CADisplayLink以特定的模式添加到RunLoop中,每当屏幕需要刷新的时候,RunLoop就会调用CADisplayLink绑定的target上的selector方法,则target就可获取CADisplayLink的每次调用的时间戳,用于准备下一帧显示的数据。可用于动画或视频。使用CADisplayLink同样要注意循环引用的问题。

CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAction)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

// displayLink.paused = YES;
// [displayLink invalidate];
// displayLink = nil;
复制代码

相比执行,NSTimer的精确度稍低,如果NSTimer的触发时间到了,而RunLoop处于阻塞状态,则其触发时间就会推迟至下一个RunLoop周期。其tolerance属性就是用于设置可以容忍的触发时间的延迟范围。

GCD Timer

使用GCD Timer则不会有这个问题,不过用法复杂不少。

NSTimer实际上依赖于RunLoop,若RunLoop对应的任务繁重,则可能导致NSTimer执行非常不准时。且NSTimer在子线程中使用需要保证该子线程常驻,即runloop一直存在。

而GCD的定时器,是依赖于内核,不依赖于RunLoop,因此通常更加准时。

- (void)setupGCDTimer {
    dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
    self.myGCDTimerQueue = dispatch_queue_create("com.icetime.mygcdtimer", attr);

    /// 创建GCD timer
    self.myGCDTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.myGCDTimerQueue);
    
    /// 设置timer
    uint64_t interval = 1 * NSEC_PER_SEC;
    dispatch_source_set_timer(self.myGCDTimer, DISPATCH_TIME_NOW, interval, 0);

    /// 设置timer的执行函数
    dispatch_source_set_event_handler(self.myGCDTimer, ^{
        NSLog(@"com.icetime.mygcdtimer");
    });

    /// 启动timer
    dispatch_resume(self.myGCDTimer);
}
复制代码
// 暂停
dispatch_suspend(self.timer);
// 销毁
dispatch_cancel(self.timer);
self.timer = nil;
复制代码
关注下面的标签,发现更多相似文章
评论
相关推荐
Android 使用 HTTPS

来源: 简书 原文: Android 使用 HTTPS 如果你的项目的网络框架是okhttp,那么使用https还是挺简单的,因为okhttp默认支持HTTPS。传送门 Android 使用 HTTP...

Android 适配一篇就够 - 编译版本?support?API 兼容?图片适配?

来源: 简书 原文: Android 适配一篇就够 - 编译版本?support?API 兼容?图片适配? 本文介绍 Android 不同系统及图片资源的常见适配问题。 compileSdkVersi...

[Kotlin Tutorials 11] Kotlin和Java的双向互操作

来源: 简书 原文: [Kotlin Tutorials 11] Kotlin和Java的双向互操作 Kotlin和Java的双向互操作 Kotlin和Java是有互操作性的(Interoperabi...

Android跳转到获取应用通知权限

来源: 简书 原文: Android跳转到获取应用通知权限 1.判断是否有通知权限 官方只最低支持到API 19(4.4),低于19的只会返回true,目前暂时没有办法获取19以下的系统是否开启了某个...

Android--PathMeasure基本用法

来源: 简书 原文: Android--PathMeasure基本用法 PathMeasure是一个用来测量Path的类 构造方法 //创建一个空的PathMeasure public PathMea...

Android屏幕适配的总结

来源: 简书 原文: Android屏幕适配的总结 屏幕适配的核心:其一,就是适配的效率,即把设计图转化为App界面的过程是否高效,其二如何保证实现UI界面在不同尺寸和分辨率的手机中UI的一致性。 背...

Android热修复之-Frameworks层修复原理分析

来源: 简书 原文: Android热修复之-Frameworks层修复原理分析 说到热修复主要有两种修复方案一种是通过dex替换的方式来达到修复效果、一种是基本native层的修复。dex替换的方式...

[译文]MongoDB WiredTiger引擎调优技巧

来源: 简书 原文: [译文]MongoDB WiredTiger引擎调优技巧 MongoDB从3.0开始引入可插拔存储引擎的概念。当前,有不少存储引擎可供选择:MMAPV1、WiredTiger、M...

知道了这些 MongoDB设计技巧,提升效率50%

来源: 掘金 原文: 知道了这些 MongoDB设计技巧,提升效率50% 范式化设计还是反范式 考虑下这样的场景,我们的订单数据是这样的 商品: { "_id": productI...

Mongo实时聚合千万文档数据

来源: 掘金 原文: Mongo实时聚合千万文档数据 1.前言 大数据的聚合分析在企业中非常有用,有过大数据开发经验的人都知道ES、Mongo都提供了专门的聚合方案来解决这个问题。但是大量数据的实时聚...

历时七天,史上最强MySQL优化总结,从此优化So Easy!

来源: 掘金 原文: 历时七天,史上最强MySQL优化总结,从此优化So Easy! 一、概述 1. 为什么要优化 一个应用吞吐量瓶颈往往出现在数据库的处理速度上 随着应用程序的使用,数据库数据逐渐增...

Mysql 百问系列:B+Tree 到底是什么

来源: 掘金 原文: Mysql 百问系列:B+Tree 到底是什么 前言: 以前看过许多关于B+ Tree的文章,当时看了总觉得明白了,可是没过多久就又要忘了。直到我看了掘金小册:Mysql是怎么运...

LRU算法及其优化策略——Mysql篇

来源: 掘金 原文: LRU算法及其优化策略——Mysql篇 在上一篇文章中,介绍了LRU算法在Redis之中的应用,本篇继续给各位道友介绍在Mysql的InnobDB引擎中,是如何使用LRU算法的。...

mysql order by 优化

来源: 掘金 原文: mysql order by 优化 version : 5.7, from 8.2.1.14 ORDER BY Optimization 本节描述MySQL何时可以使用索引来满足...

MYSQL-多表查询

来源: 掘金 原文: MYSQL-多表查询 首先创建数据表tb_departments create table tb_departments (dept_id INT PRIMARY KEY, de...

MySQL索引和SQL调优

来源: 掘金 原文: MySQL索引和SQL调优 [TOC] MySQL索引和SQL调优 本文有参考网上其他相关文章,本文最后有附参考的链接 MySQL索引 MySQL支持诸多存储引擎,而各种存储引擎...

iOS 底层拾遗:AutoreleasePool

来源: 掘金 原文: iOS 底层拾遗:AutoreleasePool 前言 在阳神的 黑幕背后的Autorelease 文章中已经把 AutoreleasePool 核心逻辑讲明白了,不过多是结论性...

iOS app秒开H5优化探索

来源: 掘金 原文: iOS app秒开H5优化探索 背景 为了快递迭代、更新,公司app有一大模块功能使用H5实现,但是体验比原生差,这就衍生了如何提高H5加载速度,优化体验的问题。此文,记录一下自...

iOS常用宏 定义

来源: 简书 原文: iOS常用宏 定义 iOS开发过程中,使用的一些常用宏定义 字符串是否为空 #define kStringIsEmpty(str) ([str isKindOfClass:[NS...

比较一下iOS中的三种定时器

来源: 掘金 原文: 比较一下iOS中的三种定时器 NSTimer NSTimer是iOS开发中的最常见的定时器。 Timers work in conjunction with run loops....