Skip to content

iOS Timer

jiaxw32 edited this page Mar 30, 2021 · 2 revisions

概述

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

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.

A timer is not a real-time mechanism. If a timer’s firing time occurs during a long run loop callout or while the run loop is in a mode that isn't monitoring the timer, the timer doesn't fire until the next time the run loop checks the timer. Therefore, the actual time at which a timer fires can be significantly later.

一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。(摘自 ibireme 《深入理解RunLoop》一文)

NSTimer is toll-free bridged with its Core Foundation counterpart, CFRunLoopTimerRef.

NSTimer 接口

@interface NSTimer : NSObject

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

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

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


/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  timeInterval  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 *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// 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 *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

/// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  fireDate   The time at which the timer should first fire.
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

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

- (void)fire;

@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;

// Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system 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 will not 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 this property.
// As the user of the timer, you will have the best idea of what an appropriate tolerance for a timer may be. A general rule of thumb, though, is to set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance will have a significant positive impact on the power usage of your application. The system may put a maximum value of the tolerance.
@property NSTimeInterval tolerance;

- (void)invalidate;

@property (readonly, getter=isValid) BOOL valid;

@property (nullable, readonly, retain) id userInfo;

@end

NSTimer 使用

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(tick:) userInfo:nil repeats:NO];
// 需要将 timer 添加到 Runloop 中才能触发
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

NSTimer 与 Runloop 关系

timer

获取 Timer 上下文信息

typedef struct {
  long _unknown; // This is always 1
  id target;
  SEL selector;
  NSDictionary *userInfo;
} _NSCFTimerInfoStruct;

- (void)tick:(NSTimer *)timer{
    CFRunLoopTimerContext context;
    CFRunLoopTimerGetContext((CFRunLoopTimerRef)timer, &context);

    if (context.info && context.retain) {
        _NSCFTimerInfoStruct infoStruct = *(_NSCFTimerInfoStruct *)(context.info);
        NSLog(@"target: %@, selector: %@, user info: %@", infoStruct.target, NSStringFromSelector(infoStruct.selector), infoStruct.userInfo);
    }
}

上述代码参考自 FBRetainCycleDetector

NSTimer 弱引用 target

示例代码如下,NSTimer 弱引用 MyView,但 timer 仍然会强持有 MyView 实例,导致 MyView 无法销毁。

@interface MyView : UIView

@property (nonatomic, weak) NSTimer *timer;

@end

@implementation MyView

- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        [self setup];
    }
    return self;
}

- (void)setup{
    __weak typeof(self) weakSelf = self;
    // 这里使用 weakSelf 不会生效,timer 仍然会强持有 self
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(tick:) userInfo:nil repeats:YES];
}

- (void)tick:(NSTimer *)timer{
    NSLog(@"%@: tick...", [timer description]);
}

- (void)dealloc{
    NSLog(@"%s", __func__);
}

@end

stackoverflow 关于 timer 弱引用 target 的说明

has the effect that (i) a weak reference is made to self; (ii) that weak reference is read in order to provide a pointer to NSTimer. It won't have the effect of creating an NSTimer with a weak reference. The only difference between that code and using a __strong reference is that if self is deallocated in between the two lines given then you'll pass nil to the timer.

timer 的正确使用方式

以下代码摘自 YYKit

@implementation MyView {
    NSTimer *_timer;
}

- (void)initTimer {
    // self 代理,YYWeakProxy 弱引用了 self
    YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
    // Runlood 强持有了 timer, timer 持有 proxy,proxy 转发 tick: 消息到 self。timer 不会强持有 self
    _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}

- (void)tick:(NSTimer *)timer {...}

- (void)dealloc{
    // self 销毁时,停用 timer,将 timer 从 Runloop 中移除
    [_timer invalidate];
}

@end

YYWeakProxy 实现如下

// YYWeakProxy.h
@interface YYWeakProxy : NSProxy

@property (nullable, nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;

@end

// YYWeakProxy.m
@implementation YYWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[YYWeakProxy alloc] initWithTarget:target];
}

- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [_target respondsToSelector:aSelector];
}

- (BOOL)isEqual:(id)object {
    return [_target isEqual:object];
}

- (NSUInteger)hash {
    return [_target hash];
}

- (Class)superclass {
    return [_target superclass];
}

- (Class)class {
    return [_target class];
}

- (BOOL)isKindOfClass:(Class)aClass {
    return [_target isKindOfClass:aClass];
}

- (BOOL)isMemberOfClass:(Class)aClass {
    return [_target isMemberOfClass:aClass];
}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
    return [_target conformsToProtocol:aProtocol];
}

- (BOOL)isProxy {
    return YES;
}

- (NSString *)description {
    return [_target description];
}

- (NSString *)debugDescription {
    return [_target debugDescription];
}

@end

参考资料

Clone this wiki locally