preface

In iOS, NSTimer is used very frequently, but NSTimer should be used carefully to avoid the problem of circular reference. I used to write like this:

- (void)setupTimer {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

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

Since self strongly references timer, and timer strongly references self, the circular reference causes the dealloc method to never walk, and neither self nor timer will be freed, causing a memory leak.

Here are a few ways to work around timer loop references.

1. Select the right time to manually release the timer(this method is not very reasonable)

This is how I solved circular references before:

  • The controller
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    [self.timer invalidate];
    self.timer = nil;
}
Copy the code
  • In the view
- (void)removeFromSuperview {
    [super removeFromSuperview];
    
    [self.timer invalidate];
    self.timer = nil;
}
Copy the code

In some cases, this works, but sometimes it can cause other problems, like when a controller pushes to the next controller, viewDidDisappear, the timer is released, and when it comes back, the timer is gone.

Therefore, this “solution” is not reasonable.

2. Timer Adds target-action in block mode

Here we need to add class methods to the classification of NSTimer ourselves:

@implementation NSTimer (BlcokTimer) + (NSTimer *)bl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void  (^)(void))block repeats:(BOOL)repeats {return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(bl_blockSelector:) userInfo:[block copy] repeats:repeats];
}

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

Get the action by block, and the actual target is set to self, which is the NSTimer class. So when we use the timer, we no longer have a circular reference because of the target change. In use, it is also necessary to pay attention to the circular reference that block may cause, so weakSelf is used:

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer bl_scheduledTimerWithTimeInterval:1 block:^{
     [weakSelf changeText];
} repeats:YES];
Copy the code

Although there are no circular references, you should remember to release the timer on dealloc.

3. Add middleware Proxy to self

Considering the reasons of circular references, the solution is to break these mutual references. Therefore, a middleware is added, which weakly references self and timer references the middleware. In this way, the mutual references are solved through weak references, as shown in the figure:

Now let’s see how to implement this middleware, directly to the code:

@interface ZYWeakObject()

@property (weak, nonatomic) id weakObject;

@end

@implementation ZYWeakObject

- (instancetype)initWithWeakObject:(id)obj {
    _weakObject = obj;
    return self;
}

+ (instancetype)proxyWithWeakObject:(id)obj {
    return [[ZYWeakObject alloc] initWithWeakObject:obj];
}
Copy the code

Adding an attribute of the weak type is not enough. In order to ensure that the middleware can respond to events from the external self, the message forwarding mechanism needs to be used to make the actual response target remain the external self. This step is crucial, mainly involving the Message mechanism of the Runtime.

/ * * * message forwarding, let _weakObject respond to events * / - (id) forwardingTargetForSelector aSelector: (SEL) {return _weakObject;
}

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

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [_weakObject respondsToSelector:aSelector];
}
Copy the code

The next step is to use middleware like this:

Self ZYWeakObject *weakObj = [ZYWeakObject proxyWithWeakObject:self]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakObj selector:@selector(changeText) userInfo:nil repeats:YES];Copy the code

conclusion

After testing, the above two schemes can solve the problem of timer’s circular reference, and I prefer the middleware method. If you have any questions, please raise them for discussion.

Code please go to Github: Demo