Some time ago, I discussed the problem of NSTimer circular citation with my colleagues in the group. At that time, I made a big talk, which was not very clear, and I didn’t know whether there was any problem with my understanding, so I made a special study and provided it to everyone

NSTimer create

  • The first kind of
self.timer = [NSTimer timerWithTimeInterval:1
                                     target:self
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
Copy the code
  • If created in the main thread, you need to change Mode to NSRunLoopCommonMode. Otherwise, NSTimer will not execute when scrolling events occur. The main thread RunLoop is enabled by default. Therefore, [[NSRunLoop currentRunLoop] run] is not required.
  • If the Mode is created in a child thread and there is no scrolling event in the current thread, you do not need to change the Mode. The RunLoop of the child thread is disabled by default and you need to manually add itRunloop:
  • The second,
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                              target:self
                                            selector:@selector(fireHome)
                                            userInfo:nil
                                             repeats:YES];
Copy the code

Circular reference analysis

When it comes to circular reference, everyone will think that weakSelf replaces self to solve the problem, for example, the writing method above is changed to

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
Copy the code

Colleague: Why does Target weakly reference NSTimer and still not release it

  • selfStrong holdtimerWe can all see it straight away, sotimerWhen is strong holdself?

In the target method, timer strongly holds the self object, thus creating a circular reference.

  • However, when we used weakSelf to break the strong reference according to the convention, we found that weakSelf did not break the circular reference, and the timer was still running.

  • Self -> Timer -> weakSelf -> self.

Take a look at__weak typeof(self) weakSelf = self;What did

It can be seen from the above figure that weakSelf and self Pointers have different addresses but the address of the memory space is the same, that is, the two objects hold the same memory space at the same time. It is equivalent to NStimer holding self indirectly, so weakSelf does not break the circular relationship

** Colleague: Do not use attributes or member variables to break the current class strong reference to timer, ok

NSTimer *timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
Copy the code

Timer still cannot be released for the following reasons

The Runloop of the main thread is not destroyed during the execution of the program, which is longer than the life cycle of self, i.e. the Runloop refers to the timer, the timer refers to the target, the target is not destroyed. The runloop refers to a timer, and the only way to break a loop is from the point where a timer refers to a target

The solution

From the above analysis, we know that what needs to be broken is the timer’s strong reference to selfThe method is as follows:

1. Use the updated API

After iOS 10, Apple optimized NSTimer to use Block callbacks to solve the circular reference problem.

[NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {/ / Do some things}].Copy the code

2. Release the timer object when the current page disappears

Use (void) didMoveToParentViewController (UIViewController *) the parent method, in this way to clean off Timer, will release the Timer object, has solved the strong reference. The dealloc method is called.

/ / life cycle to remove childVC - (void) didMoveToParentViewController: (UIViewController *) parent {if (the parent = = nil) { [self.myTimer invalidate]; self.myTimer = nil; }}Copy the code

Disadvantages: Not suitable for PresentVC

3. Middleware approach

// Define a middleware attribute @property (nonatomic, strong) id target; _target = [NSObject new]; class_addMethod([_target class], @selector(testTimer), (IMP)timerIMP, "v@:"); // use _target instead of self. . It's not the circular reference self myTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: _target selector: @ the selector (testTimer) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.myTimer forMode:NSDefaultRunLoopMode]; void timerIMP(id self, SEL _cmd) { NSLog(@"Do some things"); } // Stop Timer - (void)dealloc {[self.myTimer invalidate]; self.myTimer = nil; NSLog(@"Timer dealloc"); }Copy the code

4. Use NSProxy

Create a new class TimerProxy that inherits from NSProxy and sets a property

// Note that you want to use weak @property (nonatomic, weak) id target;Copy the code

Implementation method forwarding

Method signature / * * * / - (NSMethodSignature *) methodSignatureForSelector: (SEL) SEL {return [self. The target methodSignatureForSelector:sel]; Forward} / * * * / - (void) forwardInvocation: (NSInvocation *) invocation {[invocation invokeWithTarget: self. The target]; } where you need it, then import the TimerProxy header file using @Property (nonatomic, strong) TimerProxy * TimerProxy; _timerProxy = [TimerProxy alloc]; _timerProxy.target = self; Self. MyTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: _timerProxy selector: @ the selector (testTimer) the userInfo: nil  repeats:YES];Copy the code

5. Customize the Timer class

  1. Custom class CustomTimer, define two attributes, including timer and target, timer is the real NSTimer object, the target here is not the target of timer, but our outside real business object
@property (nonatomic, strong) NSTimer *timer; @property (nonatomic, weak) id target;Copy the code
  1. Self serves as the timer target, implements the timer callback method handler:, and creates an NSInvocation object for external method invocation from the real Target, which is passed by the userInfo parameter
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval
                                   target:(id)target
                                 selector:(SEL)selector
                                  repeats:(BOOL)repeat
{
    self = [super init];
    if (self) {
        NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
        invocation.selector = selector;
        invocation.target = target;
        self.target = target;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(handler:) userInfo:invocation repeats:repeat];
    }
    return self;
}
Copy the code
  1. To deal with the callback

In Handler: check if the weak reference target is empty. If it is not empty, the Target method is called via the Invocation. If it is empty, release the timer

- (void)handler:(NSTimer *)timer
{
    NSInvocation *invocation = [timer userInfo];
    if (self.target) {
        [invocation invoke];
    }else{
        [self invalidate];
    }
}

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

6. Similar to system API, use block mode

Using NSTimer’s Category plus block

@implementation NSTimer (BlockTimer)

+ (NSTimer *)bt_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block{
    
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(handler:) userInfo:[block copy] repeats:repeats];
}

+ (void)handler:(NSTimer *)timer{
   
    void (^block)(NSTimer *timer) = timer.userInfo;
    if (block) {
        block(timer);
    }
}
@end
Copy the code