Memory Management-Timer Problem

Circular Reference Problem of CADisplayLink and NSTimer

CADisplayLink is a timer based on QuartzCore framework, which is used in drawing-related processing. NSTimer should be familiar to everyone. It is our most commonly used timer. These two timers provide the following two API s

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

Both APIs have target parameters, which are strongly referenced by CADisplayLink/NSTimer. If CADisplayLink or NSTimer are strongly referenced by a view controller VC as attributes, when we call the above two APIs, the target parameter passes VC, thus a reference loop will be formed between VC and ADisplayLink/NSTimer, which can not be released, causing memory leaks. The figure is shown below.

Solution 1 of NSTimer
Adding NSTimer by using other API s, such as

 (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

And self is wrapped as a weak pointer and passed into it through _weak typeof (self) weak self== self.

Solution 2 of NSTimer
Break the reference loop by adding an intermediate proxy object. Look at the picture below.
As shown in the figure above, a proxy object otherObject is added between timer and VC. The strong pointer target of timer points to otherObject and the weak pointer target of otherObject points to VC, which successfully breaks the reference cycle. We need a third party to break the ring, because NSTimer is not open source, we can not modify the strength of its internal target. Therefore, only one layer of reference transit can be done through a custom proxy object, and finally the reference cycle can be broken.

Now there is another detail to deal with. Before adding the proxy object otherObject, the timer directly invokes the timer method in the VC through the target. Now there is a layer of otherObject in the middle. How to call the timer method? Actually, there are many ways to solve this problem. I believe everyone can come up with some solutions. Here is a direct recommendation of a more clever method - through message forwarding. The following figure
Because the essential purpose of the proxy object is to break the reference cycle, and pass the method to understand. OC message mechanism On the premise of principle, you should have a good understanding of the role of message forwarding, which can be skillfully used in this scenario. Please have a good experience.

Here's a code case

#import "ViewController.h"
#import "CLProxy.h"

@interface ViewController ()
//@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //CADisplayLink is used to ensure that the call frequency is consistent with the screen brush frequency, 60FPS
//    self.link = [CADisplayLink displayLinkWithTarget:[CLProxy proxyWithTarget:self] selector:@selector(linkTest)];
//    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

//- (void)linkTest {
//    NSLog(@"%s",__func__);
//}

- (void)timerTest {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    NSLog(@"%s",__func__);
}

@end

****************🥝🥝🥝🥝proxy class CLProxy🥝🥝🥝
**************** CLProxy.h  ****************
#import <Foundation/Foundation.h>

@interface CLProxy : NSObject
+(instancetype)proxyWithTarget: (id)target;
@property (weak, nonatomic) id target;

@end

**************** CLProxy.m  ****************
#import "CLProxy.h"
@implementation CLProxy

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


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

The scheme is also applicable to CADisplayLink, and it is no longer discussed.

Understanding NSProxy
You may have seen a class called NSProxy, but it should rarely be used. It's a very special class. Let's compare it with the definition of NSObject.

@interface NSProxy <NSObject> {
    Class	isa;
}

@interface NSObject <NSObject> {
    Class isa  ;
}

As you can see, NSProxy and NSObject are at the same level, so you can also understand NSProxy as a base class. They all abide by the < NSObject > protocol, and they have no parent class.

So what is NSProxy for? In fact, it is designed to solve the problem of forwarding messages through intermediate objects.

Here we post the case code first.

#import "ViewController.h"
#import "CLProxy2.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
}
@end

****************🥝🥝🥝🥝proxy class CLProxy🥝🥝🥝
**************** CLProxy2.h  ****************
#import <Foundation/Foundation.h>

@interface CLProxy2 : NSProxy
+(instancetype)proxyWithTarget: (id)target;
@property (weak, nonatomic) id target;
@end

**************** CLProxy2.m  ****************

#import "CLProxy2.h"

@implementation CLProxy2

+(instancetype)proxyWithTarget: (id)target {
//The NSProxy object does not need to call init because it has no init method and can be used directly after alloc.
    CLProxy2 *proxy = [CLProxy2 alloc];
    proxy.target = target;
    return proxy;
    
}

@end

CLProxy 2 inherits from NSProxy. First of all, it links VC, timer and CLProxy 2 in the same way as the previous case. First, we don't do anything about the message in CLProxy 2. Let's see what happens. The result is the error message.

2019-08-26 11:26:13.486949+0800 memory management[3407:219430] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy methodSignatureForSelector:] called!'

As you can see, sending a method (message) to the CLProxy2 object that it did not implement will finally call the methodSignatureForSelector method. If you are familiar with ** [OC messaging mechanism]**, send a message to an instance object of a class inherited from NSObject. If the object does not implement the corresponding method, the error will be

2019-08-26 11:31:01.254135+0800 memory management[3456:222524] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CLProxy timerTest]: unrecognized selector sent to instance 0x600000d64210'

That is the classic unrecognized selector sent to instance.
What's the matter? In fact, the processing flow of NSProxy after receiving the message is as follows

  • [proxyObj message]
  • (1) Look for the corresponding method in the class object of proxyObj and call it if you find it.
  • (2) Attempt to enter the parent object recursive lookup method (omit this step)
  • (3) unable to find a method, try to do dynamic analysis of the method (omit this step)
  • (4) Attempt to call forwarding Target ForSelector for message forwarding `(omit this step)
  • (5) Attempt to call method Signature ForSelector + forward Invocation for message forwarding.

Therefore, compared with the complete message mechanism process, steps (2), (3) and (4) are omitted in the processing of NSProxy. So it is more efficient than NSObject. The problem of proxy object messaging discussed today can be solved by NSProxy to improve efficiency. To eradicate step (5), we only need to implement the method Signature ForSelector + forward Invocation methods in the subclass. The CLP Roxy 2.m code above can be modified as follows

#import "CLProxy2.h"

@implementation CLProxy2

+(instancetype)proxyWithTarget: (id)target {
//The NSProxy object does not need to call init because it has no init method and can be used directly after alloc.
    CLProxy2 *proxy = [CLProxy2 alloc];
    proxy.target = target;
    return proxy;
    
}


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

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

@end

Later, when we encounter similar scenarios where messages are delivered through intermediary objects, the most recommended one is to use NSProxy.

If someone asks you if CADisplayLink and NSTimer are on time?

Believe in the answer, everyone will say: not on time. But the reason for the punctuality may not be clear to everyone. So here's a brief comb.
The bottom layers of CADisplayLink and NSTimer are implemented by RunLoop, which can be understood as events that RunLoop needs to handle. We know that RunLoop can be used to refresh UI, handle timers (CADisplayLink, NSTimer), handle click-and-slide events, and many other things. Here, you need to understand how RunLoop triggers the NSTimer task. RunLoop handles certain events and takes a certain amount of time for each cycle, but the exact amount of time is uncertain.
If you turn on a timer and trigger the timer event every second, RunLoop will start accumulating the time spent in each cycle, when the time accumulates enough for one second, the timer event will be triggered. If you are interested, you can find time to accumulate the relevant code in RunLoop's source code. You can deepen your understanding with the help of the following figure
If RunLoop's tasks are too heavy in a particular circle, the following may occur

So CADisplayLink and NSTimer can't guarantee punctuality.

GCD Timer

GCD timers are directly linked to the system kernel, independent of the RunLoop mechanism, so the time is quite accurate. The GCD timer is very simple to use, as shown below.

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //Initialization timer
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    //start time
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 3.0*NSEC_PER_SEC);
    //Interval time
    uint64_t intervalTime = 1.0;
    //Error time
    uint64_t leewayTime = 0;
    //Setting Timer Time
    dispatch_source_set_timer(self.timer, startTime, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //Setting Timer Callback Events
    dispatch_source_set_event_handler(self.timer, ^{
        //Timer Event Code
        NSLog(@"GCD timer event");
        //If the timer does not need to be repeated, you can cancel the timer here.
        dispatch_source_cancel(self.timer);
    });
    //Running Timer
    dispatch_resume(self.timer);
    
}

GCD Timer Details: We were in the Chapter Run Loop In the same scenario (GCD timer is the main thread), GCD timer will not be impressed by the sliding UI interface. The fundamental reason is that GCD timer has nothing to do with RunLoop. They are two independent mechanisms, so GCD timer is not impressed by the sliding UI interface. It is not bound by RunLoopMode. You can experience it by your own code.

In addition, it should be noted that in the ARC environment, some objects created in the GCD do not need to be destroyed. GCD has already helped us with memory management.

Posted by squiblo on Thu, 29 Aug 2019 22:20:10 -0700