iOS development NSTimer timer

preface

In the process of iOS development, we often use three kinds of timers: NSTimer timer, CADisplayLink timer and GCD timer.
This article only introduces NSTimer timer.

What is NSTimer

runLoop literally means a loop in operation. Its function is to keep the APP running continuously and deal with various events of the APP, such as click event, timer event and Selector event, so as to save CPU resources and improve the performance of the APP. It can make the thread busy when it is working and sleep when it is not working.

NSTimer is a class provided by the Foundation library and is implemented based on runloop. It can be executed only once or repeatedly on a regular basis (set the repeat parameter). If it is executed only once, it will be automatically destroyed after execution. For repeated execution, you must manually call invalidate to destroy it.

How to use NSTimer

Create NSTimer

The system provides 8 creation methods: 6 class creation methods and 2 instance initialization methods. Among them, the scheduled initialization method will be directly added to the current runloop with NSDefaultRunLoopMode; If you do not need to initialize in scheduled mode, you need to manually add the timer: formode: to a runloop.

Describe all creation method parameters:

  • ti(interval): timer trigger interval, in seconds, which can be decimal. If the value is less than or equal to 0.0, the system will assign 0.1 ms by default
  • invocation: this form is rarely used. Most of them are in the form of block and aSelector
  • yesOrNo(rep): whether to repeat. If YES, trigger repeatedly until the invalidate method is called; If the NO method is called once, it will be triggered automatically
  • aTarget(t): the target to send the message. The timer will strongly reference aTarget until the invalidate method is called
  • aSelector(s): message to be sent to aTarget
  • userInfo(ui): user information passed. If it is used, aSelector must have a parameter declaration first, and then it can be obtained through [timer userInfo], or nil, then [timer userInfo] is empty
  • Date: trigger time. Generally, we write [NSDate date]. In this case, the timer will trigger once immediately and take this time as the benchmark. If there is no method of this parameter, it is based on the current time, and the first trigger time is the current time plus the time interval ti
  • block: timer will execute this operation when triggered, with a parameter and no return value (Note: when added to runloop, the parameter timer cannot be empty, otherwise an exception will be thrown)

In the following three creation methods, the timer is automatically added to the current runloop. The mode is NSDefaultRunLoopMode:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

The following five creation methods will not be automatically added to runloop. Instead, you need to call - (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
 - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

Trigger NSTimer

  1. Appointment trigger: after NSTimer is created and added to runloop, it will be triggered after the appointment time when we create it.
  2. Trigger immediately: - (void)fire. The impact of this trigger mode on the timer can be divided into two cases:
    (1). For the timer set to YES for repeated execution, the original timer still takes effect after being triggered immediately;
    (2). For the repeated execution of the timer set to NO, the original timer will expire after being triggered immediately.

Destroy NSTimer

Destroying NSTimer calls - (void)invalidate, which is the only method that can remove the timer from runloop. The time of destruction is when the target is released, so how can the capture target be released? If it is a controller, you can try to listen to the call of pop method (nav proxy) or viewDidDisappear method. If it must be written in the dealloc method, it must be ensured that there is no circular reference, because when the circular reference exists, the dealloc method will not go.

NSTimer and runloop

Nstimers created in either the main thread or the sub thread need to be added (either actively or passively) to the corresponding runloop.

Thread and runloop are one-to-one correspondence, and live and die together. A timer can only be added to one runloop, that is, a timer can only be added to one thread. If it has to be added to multiple runloops, only one is valid.

Note: the runloop in the main thread is started actively, and the runloop in the sub thread needs to be started manually by calling the run method ([[nsrrunloop currentrunloop] run]).

NSTimer and performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay: an NSTimer is created internally and then added to the runloop of the current thread.
Q: delaying the execution of performSelector:withObject:afterDelay in the child thread: will the selector method be called?
A: No. Because NSTimer is delayed in the current runloop, and the runloop of the child thread is not enabled by default, it cannot respond to the selector method. You need to add [[NSRunLoop currentRunLoop] run] to start the runloop of the child thread, or implement the delayed execution in the main thread.

Note: if you want to start the runloop of a thread, you must have any event of timer, source and observer to start it. That is, before executing [[NSRunLoop currentRunLoop] run], if no event is added to the current runloop, the runloop of the thread will not be started.

NSTimer considerations

  1. timer must be added to Runloop.
    Because Timer is also a resource, the function of this resource must be added to runloop. Similarly, if the runloop does not contain any resources, running the runloop will exit immediately.
  2. The timer event will be responded to only when the mode of the timer is the same as that of the current runloop.
    When the runloop of the same thread is running, it can only be in one of these modes at any time. Therefore, only when runloop is in the mode of timer can timer get the chance to trigger the event. If you are not in timer mode, you cannot respond to timer events. NSRunLoopCommonModes is not a real mode. It belongs to the placeholder mode (both NSDefaultRunLoopMode and uitrackingrunnloopmode will execute).
  3. timer creation and destruction must be in the same thread.
  4. timer is not a real-time mechanism, and there may be errors in the time interval of NSTimer callback.
    Because RunLoop checks whether the current cumulative time has reached the interval set by the timer after each lap. If not, RunLoop will enter the next round of task and check the current cumulative time after the task is completed. At this time, the cumulative time may have exceeded the interval of the timer, so there may be errors.
    Solution: replace it with GCD timer.
  5. Timer will have a strong reference to target. If target has a strong reference to timer, a circular reference will be triggered.
    terms of settlement:
    (1) Use block.
    Add the classification method to NSTimer, call [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(repeatAction:) userInfo:timerBlock repeats:repeats] in the classification method, change target from the previous controller object to the timer object, and transfer the target as the parameter. It not only solves the circular reference, but also unifies the creation of timer and the location of event processing, which improves the readability of the code.

//.h

typedef void(^TimerBlock)(void);

@interface NSTimer (CicularReference)

///timer uses block method to add target action to solve circular reference
+ (NSTimer *)scheduledTimerWithInterval:(NSTimeInterval)interval block:(TimerBlock)timerBlock repeats:(BOOL)repeats;

@end

//.m

@implementation NSTimer (CicularReference)

+ (NSTimer *)scheduledTimerWithInterval:(NSTimeInterval)interval block:(TimerBlock)timerBlock repeats:(BOOL)repeats
{
    NSTimer *timer = [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(repeatAction:) userInfo:timerBlock repeats:repeats];
    
    return timer;
}

+ (void)repeatAction:(NSTimer *)timer
{
    TimerBlock timerBlock = timer.userInfo;
    if (timerBlock) {
        timerBlock();
    }
}

@end

//Use

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.timer
     = [NSTimer scheduledTimerWithInterval:1
                                  block:^{
        [self timerDoing];
        
    }
                                repeats:YES];
}

- (void)timerDoing
{
    NSLog(@"Timing operation");
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

(2) Target uses a proxy object and the target property is set to a weak pointer.

Method 1: inherit NSObject

//.h

@interface MxProxy : NSObject

//Weak pointer
@property (nonatomic, weak) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end

//.m

@implementation MxProxy

+ (instancetype)proxyWithTarget:(id)target
{
    MxProxy *proxy = [[MxProxy alloc] init];
    proxy.target = target;
    
    return proxy;
}

//Message forwarding
- (id)forwardingTargetForSelector
{
    return self.target;
}

@end

//Use

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    //proxy:NSObject
    MxProxy *proxy = [MxProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(timerDoing) userInfo:nil repeats:YES];
}

- (void)timerDoing
{
    NSLog(@"Timing operation");
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

@end

Method 2: inherit NSProxy

//.h

//MfnProxy
@interface MfnProxy : NSProxy

//Weak pointer
@property (nonatomic,weak) id target;

+(instancetype)proxyWithTarget:(id)target;

@end

//.m

@implementation MfnProxy

+(instancetype)proxyWithTarget:(id)target
{
    MfnProxy *proxy = [MfnProxy alloc];
    proxy.target = target;
    return proxy;
}

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

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

@end

//Use

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    //proxy:MfnProxy
    MfnProxy *proxy1 = [MfnProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy1 selector:@selector(timerDoing) userInfo:nil repeats:YES];
}

- (void)timerDoing
{
    NSLog(@"Timing operation");
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

@end

Keywords: iOS xcode objective-c

Added by rashmi_k28 on Tue, 08 Feb 2022 12:50:12 +0200