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
- Dereferencing
- 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.