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

5,379

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;