NSTimer is used frequently in long time development, but a careless may cause a small problem, and more attention is needed in daily use.

Is NSTimer accurate

Q: is NSTimer accurate?

Answer: NSTimer is generally inaccurate if used as follows:

/ / main thread invokes the _timer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @ the selector (logInfo) userInfo:nil repeats:YES]; // Call _timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(logInfo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

Copy the code

The above two methods of direct call NSTimer is likely to be inaccurate accounting, for the following reasons:

  1. NSTimer is processed in each runloop, but when there are other time-consuming operations in the Runloop that take longer than the NSTimer interval, the NSTimer is processed later. Cause inaccuracy. Workaround: You can put NSTimer in the child thread and manually enable runloop for the child thread. There are no other time-consuming operations in the current runloop, so it will be relatively accurate.

  2. When not specified, runloop is added to RunLoopDefaultMode by default. When the page has a TableView slide, the main thread runloop switches to TrackingRunLoopMode. In this mode, NSTimer will not be triggered, resulting in inaccurate timing.

NSTimer memory leak

> > < span style = “padding-bottom: 0px; padding-bottom: 0px;

If repeats is NO, there is NO memory leak and the timer will automatically expire after the call is completed.

Problems caused:

In general, NSTimer is used as follows:

self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Copy the code

As the code above reads, the timer will be invoked normally, but after exiting the current page or function, the timer will still be invoked, and does not stop. To stop, manually call the [self.timer invalidate] method.

You might think of something like this:

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}
Copy the code

This method is not a problem at first glance, not to manually call the stop timer method, then when the page release, call good.

When tested, the current object self will not be released, and the timer will not be released, causing a memory leak.

Analysis problem:

Take a look at the previous code:

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

self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Copy the code

Self holds the timer, and the timer holds self in the target parameter

This leads to cross-referencing.

Some of you might be confused right away:

  1. selfDon’t holdtimerCan also be used normally, why to hold
  2. timerWhy do they holdself

Answer the above two questions:

  1. selfholdtimerIt needs to be called manually somewhere else[self.timer invalidate].selfDon’t holdtimerWell, it’s easy to solve the cross-reference problem, buttimerYou can’t release it manually.
  2. runloopThe need totimerA reserved operation is performed by the observer at the specified point in time.
To solve the problem

There are several ways to solve the above memory leak problem:

  1. viewWillDisappear:Manual stop
  2. Override return method to manually stop
  3. After iOS10, the system provides itscheduledTimerWithTimeInterval:repeats:blockmethods
  4. Get rid ofNSTimerAnd the current calling object.

Detailed explanation:

Manual stop timer, to have a time. If according to the demand, just when the user can operate a function can be actively stopped, is a better way. However, if this opportunity is not available, it can be solved in methods 1 and 2.

  1. ViewWillDisappear: Stops manually

    - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
        [self.timer invalidate];
        self.timer = nil;
    }
    Copy the code

    Note that stopping in this method is useful if you don’t push the next page from the current one, because viewWillDisappear: will also be called if you push the next page and may not get the correct result.

  2. Override return method to manually stop

    - (void)backButtonPressed:(id)sender {
        [self.timer invalidate];
        self.timer = nil;
    }
    Copy the code
  3. Apple presumably also found that timer was prone to memory leaks, so after iOS10, there was a new API that had no memory leaks.

    if(@ the available (iOS 10.0, *)) { self.timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"do more thing");
        }];
    }
    
    - (void)dealloc
    {
        [self.timer invalidate];
        self.timer = nil;
    }
    Copy the code

    Calling this new API will not enforce references to each other, and the dealloc method will be called normally.

  4. Removes the circular reference between NSTimer and the currently calling object. We can take a cue from the system method in 3. Instead of referring directly to self, the method in 3 makes the timer refer to another object, thus removing the cross-reference. For iOS10 and below, you can imitate the implementation of the system.

    #import <Foundation/Foundation.h>
    
     NS_ASSUME_NONNULL_BEGIN
    
     @interface NSTimer (weak)
    
     + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block;
    
     @end
    
     NS_ASSUME_NONNULL_END
    Copy the code
    #import "NSTimer+weak.h"
    
    @implementation NSTimer (weak)
    
    + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
         return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(weak_blcokInvoke:) userInfo:[block copy] repeats:repeats];
    }
    
    + (void)weak_blcokInvoke:(NSTimer *)timer {
     
        void (^block)(NSTimer *timer) = timer.userInfo;
     
         if (block) {
             block(timer);
         }
     }
    
     @end
    Copy the code

    In use, it can be as follows:

    self.timer = [NSTimer weak_scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
         //do some thing
    }];
     
    - (void)dealloc
     {
         [self.timer invalidate];
         self.timer = nil;
     }
    Copy the code

    You can see that target is converted from the original VC object to a timer object, breaking the bidirectional reference.