Strong reference problem

Usually we use NSTimer or CADisplayLink, if you do not handle directly using the system provided API methods, there may be a strong reference problem (note that strong reference non-circular reference).

Scenario: Controller A -> push -> controller B. The implementation of controller B is as follows:

#import "ViewControllerB.h" @interface ViewController () @property (strong, nonatomic) NSTimer *timer; @end @implementation ViewControllerB - (void)viewDidLoad { [super viewDidLoad]; / / every two seconds to invoke a timerTest self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @ the selector (timerTest) userInfo:nil repeats:YES]; } - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; } @endCopy the code

When controller A enters controller B, the timer starts to work. However, when you click “Return” and return to page A from page B, you will find that the dealloc method of controller B is not called, indicating that controller B is not destroyed.

So why?? Is it circular references?? Well, it looks like, because controller B has a strong reference to the timer, and when the timer is created, it has a strong reference to target, which is controller B, which produces a strong reference. Many articles on the Internet also explain this, in fact, can not be wrong, because it does seem to have circular references. But when you change controller B’s reference to the Timer to a weak reference:

@property (weak, nonatomic) NSTimer *timer;
Copy the code

You’d be surprised to find that the same problems still exist. So why is this ???? Controller B is supposed to have a weak reference to the timer, so when the – (void)viewDidLoad method executes, the scope of the timer ends and it should die, but it doesn’t, which means there should be a strong reference to the timer. In fact, this other object is actually a Runloop object. Source code proof (reference from GNUStep) :

+ (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti target: (id)object selector: (SEL)selector userInfo: (id)info repeats: (BOOL)f { id t = [[self alloc] initWithFireDate: nil interval: ti target: Object // Timer strongly references object selector: selector userInfo: info repeats: f]; [[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode]; RELEASE(t); return t; }Copy the code

As can be seen, after the timer is created, it is directly added to the current Runloop. Thus, the reference relationship between controller B, timer, and Runloop can be obtained: Runloop => timer => controller B. So the reason controller B can’t be destroyed is because the timer has a strong reference to it.

So what’s the solution? Change the timer’s reference to controller B to a weak reference. The specific solution can be found on the Internet.

Solution 1: Change the method, using the block way to achieve. (Simple and clear) implementation is as follows:

#import "ViewController.h" @interface ViewController () @property (strong, nonatomic) NSTimer *timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Weaktype (self) weakSelf = self; // Weaktype (self) weakSelf = self; The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {[weakSelf timerTest]; }]; } - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; } @endCopy the code

Scheme 2:Add a proxy object. As follows:

Implementation code:

@interface MJProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end #import "MJProxy. H "@implementation MJProxy + (instanceType)proxyWithTarget (id)target { MJProxy *proxy = [MJProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @end #import "ViewController.h" #import "MJProxy.h" @interface ViewController () @property (strong, nonatomic) NSTimer *timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: [MJProxy proxyWithTarget: self] selector:@selector(timerTest) userInfo:nil repeats:YES]; } - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; } @endCopy the code

The same goes for the use of CADisplayLink.

#import "ViewController.h" #import "MJProxy.h" @interface ViewController () @property (strong, nonatomic) CADisplayLink *link; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Make sure the call frequency is consistent with the screen's brush frame frequency. 60FPS self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)]; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } - (void)timerTest { NSLog(@"%s", __func__); } - (void)linkTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.link invalidate]; } @endCopy the code

Incorrect timing problem

The NSTimer timer is incorrect. The reason is related to Runloop. The NSTimer is dependent on Runloop. Once the NSTimer is created, it needs to be added to the Runloop, and the Runloop checks the NSTimer each time to see if it needs to perform the corresponding task. However, each loop of a Runloop takes a variable amount of time. If there are more tasks, the time may be longer, and if there are fewer tasks, the time may be shorter. This is why NSTimer has errors.

Bottom line: NSTimer depends on RunLoop, and if RunLoop’s tasks are too heavy, the NSTimer may not be on time

So how do you get accurate timing in iOS? You can use a GCD timer (which has nothing to do with Runloop and calls kernel functions directly).

#import "ViewController.h" @interface ViewController () @property (nonatomic, strong) dispatch_source_t timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Dispatch_queue = dispatch_queue_create("com.long", DISPATCH_QUEUE_SERIAL); Dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // Set uint64_t start = 2; // Uint64_t interval = 1; // Dispatch_source_set_timer (timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0); // Set event callback dispatch_source_set_event_Handler (timer, ^{NSLog(@"11111111"); // The task to be performed}); // Dispatch_source_set_event_handler_f (timer, fireTimer); // Start the timer dispatch_resume(timer); self.timer = timer; Void fireTimer() {NSLog(@"11111111"); // The task to perform} @endCopy the code

In order to be convenient to use later, the timer of GCD is encapsulated below.

.h files

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LCTimer : NSObject

+ (NSString *) execTask:(void(^)(void))task
                  start:(NSTimeInterval)start
               interval:(NSTimeInterval)interval
              repeating:(BOOL)repeating
                  async:(BOOL)async;


+ (NSString *) execTask:(id)target
                  selector:(SEL)selector
                  start:(NSTimeInterval)start
               interval:(NSTimeInterval)interval
              repeating:(BOOL)repeating
                  async:(BOOL)async;

+ (void)cancelTask:(NSString *)task;

@end

NS_ASSUME_NONNULL_END
Copy the code

.m files

#import "LCTimer.h" static NSMutableDictionary *timerMap_; static dispatch_semaphore_t semaphore_; @implementation LCTimer + (void) Initialize {static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ timerMap_ = [[NSMutableDictionary alloc] init]; semaphore_ = dispatch_semaphore_create(1); }); } + (NSString *)execTask:(void(^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeating:(BOOL)repeating async:(BOOL)async { if (! task || start < 0 || (repeating && interval <= 0)) return nil; // Create a serial queue of dispatch_queue_t queue = async? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue(); Dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); Dispatch_source_set_timer (timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0); dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER); NSString *taskId = [NSString stringWithFormat:@"%zd", timermap_.count]; [timerMap_ setObject:timer forKey:taskId]; dispatch_semaphore_signal(semaphore_); // Set the event callback dispatch_source_set_event_handler(timer, ^{task(); if (! repeating) { [self cancelTask:taskId]; }}); // Start the timer dispatch_resume(timer); return taskId; } + (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval  repeating:(BOOL)repeating async:(BOOL)async { if (! target || ! selector) return nil; return [self execTask:^{ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if  ([target respondsToSelector:selector]) { [target performSelector:selector]; } #pragma clang diagnostic push } start:start interval:interval repeating:repeating async:async]; } + (void)cancelTask:(NSString *)task { if (task.length <= 0) return; dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER); dispatch_source_t timer = [timerMap_ objectForKey:task]; if (timer) { dispatch_source_cancel(timer); [timerMap_ removeObjectForKey:task]; } dispatch_semaphore_signal(semaphore_); } @endCopy the code

Simple use:

#import "ViewController.h"
#import "LCTimer.h"

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, strong) NSString *taskId;
@property (nonatomic, strong) NSString *taskId2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.taskId = [LCTimer execTask:^{
        NSLog(@"222222 %@", [NSThread currentThread]);
    } start:2 interval:1 repeating:YES async:NO];
    
    self.taskId2 = [LCTimer execTask:self selector:@selector(justForTest) start:2 interval:1 repeating:YES async:YES];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [LCTimer cancelTask:self.taskId];
    [LCTimer cancelTask:self.taskId2];
}

- (void)justForTest {
    NSLog(@"555555555");
}
@end
Copy the code

reference

MJ Basic Courses