NSTimer is a very important part of knowledge, and we need to have a good understanding of this part and the circular reference part

Problem is introduced into

We’ve started with a couple of questions

  • What is a circular reference?
  • Why does NSTimer want to have a strong reference to target?
  • Is there a better way to solve this circular reference?

A circular reference

Simple understanding of circular references

The simple way to think about it is

Object A holds object B, and object B holds object A

Maybe it’s a little bit abstract but I can give you an example

@interface FirstPerson : NSObject 
 
@property (nonatomic.strong) SecondPerson *test; 
 
@end 
Copy the code
@interface SecondPerson : NSObject 
 
@property (nonatomic.strong) FirstPerson *test; 
 
@end 
Copy the code

If it’s not A circular reference, it’s A one-way holding, so let’s say A holds B, so B is A property of A

If A release message is sent to object A, and object B is found to have A release message, the release message is sent to object B. Object B executes the dealloc method, the reference count is 0, and memory is freed. At the same time, the reference count of object A is also 0, and memory is freed correctly

If it’s a circular reference

If A sends A release message and finds that B is in possession of it, it sends A release message to B and waits for B to release its memory. After receiving the release message, B finds that it has A, so it sends A release message to A and waits for A to release the memory. In this case, A is waiting for B to release memory, and B is waiting for A to release memory, resulting in A deadlock and A memory leak

We can break this circular reference if we use Weak

If A sends A release message and finds that B is in possession of it, it sends A release message to B and waits for B to release its memory. After receiving the release message, B holds A (weak), but does not wait for A to free memory. In this case, the reference count is 0 and dealloc is executed to free memory. In this case, the reference count of A becomes 0 and dealloc is executed to free memory

Circular references in a Block dance with each other

If we declare a block in a property on a page, then it’s like our page self is holding the block and if we’re holding self in the block, then it’s going to cause a circular reference

What’s the solution?

Just have a “false self” (weak quote)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
      __weak typeof(self) weakSelf = self;
      self.blk = ^{
          // Execute after 5 seconds
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              NSLog(@ "% @", weakSelf);
          });
      };
      self.blk();
      [self dismissViewControllerAnimated:YES completion:nil];
}
Copy the code

The self object holds a block, and the block holds self. To break the circular reference, it’s ok to use Weak

But what if it’s in a new view? What happens when we dismiss the view directly after executing the block?

In the case of nothing, when we dismiss, we don’t take dealloc, and the internal loop is waiting for each other, causing the page to dismiss, but the dealloc method doesn’t take, resulting in a memory leak.

We just add weak

Five seconds later I print self, which is this new view, the view is dismissed, it doesn’t exist, and I print self, which is definitely null

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
      __weak typeof(self) weakSelf = self;
      self.blk = ^{
          __strong typeof(self) strongSelf = weakSelf;
          // Execute after 5 seconds
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              NSLog(@ "% @", weakSelf);
          });
      };
      self.blk();
      [self dismissViewControllerAnimated:YES completion:nil];
}
Copy the code

That will do

Weak on the outside is to break the circular reference, strongSelf on the inside is to make the object on the new page retain+1, and when the object is released on the outside, its reference count is not zero at this point, so it cannot be released. Since strongSelf is stack allocated memory, once the execution is complete, the object’s reference count becomes zero and memory is freed.

NSTimer

So that’s the end of what we’ve been doing with circular references, so let’s move on to NSTimer

Create NSTimer

Create NSTimer commonly used method is + (NSTimer *) scheduledTimerWithTimeInterval (NSTimeInterval) ti target: (id) target selector (SEL) aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

Less common is + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

and

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

What are the differences between these methods in addition to the creation method (parameters) and method types (class methods, object methods)?

We commonly used scheduledTimerWithTimeInterval compared with other two methods, it is not only created the NSTimer objects, also add the object to the current runloop

NSTimer does not take effect until it is added to the runloop, and the NSTimer is executed.

That is, if we want to use timerWithTimeInterval or initWithFireDate, Add NSTimer to the runloop using the following method of NSRunloop – (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode

Destruction of NSTimer

I didn’t think about it before, but the idea was I just put it nil and we’re done, and the system will recycle it for us, right

But that’s not how it works

Invalidate with fire

Take a look at what these two mean in the official document

  • invalidate

Stops the receiver from ever firing again and requests its removal from its run loop

This method is the only way to remove a timer from an NSRunLoop object

Prevents the receiver from firing again and requests that it be removed from the Runloop

This method is the only way to remove the timer from the NSRunLoop object

  • fire

Causes the receiver’s message to be sent to its target

If the timer is non-repeating, it is automatically invalidated after firing

Causes the receiver’s message to be sent to its destination

If the timer is not repeated, it will automatically expire after it is triggered

In general, if you want to destroy an NSTimer, always use the invalidate method

Invalidate and = nil

Just like we destroy other strongly referenced objects, can we just set NSTimer to nil and have iOS destroy NSTimer for us?

The answer is no

Why is that?

Let’s take a look at the reference count under ARC

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSLog(@"Retain count is %ld".CFGetRetainCount((__bridge CFTypeRef)self));

    _timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerSelector) userInfo:nil repeats:NO];

    NSLog(@"Retain count is %ld".CFGetRetainCount((__bridge CFTypeRef)self));

    [_timer invalidate];

    NSLog(@"Retain count is %ld".CFGetRetainCount((__bridge CFTypeRef)self));

}

- (void) timerSelector {
    NSLog(@ "--");
}
Copy the code

We print the reference count for self (which is the current VC)

When you add NSTimer, the reference count goes from 29 to 30.

The fact that the timer has a strong reference to VC. Because if you want the timer to run and execute timerSelector: under the VC, the timer needs to know the target and save the target. In order to execute the target performSelector later, the target is the VC (self).

So, NSTimer and VC are strongly referenced to each other. This seems to create the problem of circular references.

To remove the circular reference, in the invalidate method, the target previously saved by the timer is set to nil, forcing the break of the circular reference. It’s not much different than just setting timer = nil. Invalidate did another thing, however, by removing runLoop’s strong reference to the timer, and the timer was successfully stopped.

We can see this in official documents

  1. Dereferencing
  2. Remove the timer from runloop (remove runloop’s strong reference to the timer)

So invalidate the timer in the dealloc of the class that created the NSTimer object. This is a given, because dealloc is strongly referenced and cannot be executed until it is invalidated

The interview questions

  • Does NSTimer have to have self?

Look at the three creation methods from the official documentation+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats All three methods, according to the official documentation, generate a strong reference to target, so whenever target-action is used, self will be referenced. Apple probably also found out about the memory leak caused by the Timer, so after iOS10, there’s a new API for these three,Calling these new apis does not cause memory leaks

  • How to solve the problem of NSTimer strong holding

When appropriate, destroy the Timer, or use a proxy object, and have the Timer reference the proxy object, which weakly references self. The best option is to use proxy object forwarding, the system-provided NSProxy (message forwarding mechanism). We’re going to say

  • Student: is there a circular reference here where self doesn’t hold NSTimer

It feels like a trap. You can’t say yes or no. You can just say it’s not about circular references. With NSTimer you need to add it to the Runloop, and if it’s target-action, NSTimer is going to refer to target which is self. This creates a runloop->timer->self that is a strong reference. No one can release it

  • Will Runloop hold NSTimer

The NSTimer does not take effect until it is added to the runloop. The NSTimer is executed, and the runloop holds the NSTimer.

How to solve the problem of NSTimer strong holding?

This is the second interview question above

Proxy object in the middle

Use the NSObject class for message forwarding

It looks like this

With target-action, when the page is destroyed, the dealloc method is not invoked, causing a circular reference

_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TestProxy proxyWithTarget:self] selector:@selector(timerSelector) userInfo:nil repeats:YES];




@interface TestProxy : NSObject

+ (instancetype) proxyWithTarget:(id)target;
@property (nonatomic.weak) id target;

@end


#import "TestProxy.h"

@implementation TestProxy

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

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

@end
Copy the code

We replace the target’s self with an intermediate class to forward the message

Print dealloc successfully

Use NSProxy class for message forwarding

This is a special class for message forwarding, and we need to use it as a subclass

Need to pay attention to

NSProxy subclasses need to implement two methods, that is, the two above, namely: methodSignatureForSelector: and forwardInvocation:

@interface TestNSProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@property (nonatomic.weak) id target;

@end




#import "TestNSProxy.h"

@implementation TestNSProxy

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

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

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

@end

Copy the code

NSProxy class advantage lies in the direct execution of the message forwarding mechanism three times to save the third step in call methodSignatureForSelector: return method signature, if you don’t is nil method signature, call forwardInvocation: to perform this method, will skip the previous steps, To improve performance

Changing a Timer reference

I started out thinking how do we solve circular references in blocks? __weak typeof(self) weakSelf = self; And then have this fake self. But the fact that we’re using target-action+ “fake self” here doesn’t work

Why is that?

We said in the block, if the outside is a strong pointer, the block reference when the strand is saved with a strong pointer, if the outside is a weak pointer, the block reference when the internal weak pointer is saved, so for the block we use weakSelf useful.

However, for CADisplayLink and NSTimer, no matter whether a weak or strong pointer is passed outside, a memory address will be passed in, and the timer will generate a strong reference to that memory address internally, so passing a weak pointer is useless.

So you can do that with blocks

__weak typeof(self) weakSelf = self;
    self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf doSomething];
    }];
Copy the code

Call the invalidate method where appropriate

This is one of the trickier ones. The best way to do that is to use the proxy object forward, the NSProxy provided by the system.