Among the questions about circular reference, the interviewer likes to ask about the circular reference of Block and Timer most. Today, we will analyze the circular reference of Timer.

General problem scenario

In the scenario where Timer is needed, the UI page generally needs timed operations, such as timed rotation of the wheel map, or timed refresh of background data and display, etc. We can roughly draw the following conclusions:

  • The thing that needs a Timer is the UI view, which is used internally;
  • It responds to UI operations, operating on the main thread.

Problem analysis

In most cases, the page (VC) is strongly referenced to the view, and the Timer is strongly referenced to its target. In addition, the Timer will be added to the RunLoop, so the RunLoop is also strongly referenced to the Timer. Even if the view is weakly referenced to the Timer, we can also get the reference analysis diagram like this:

In the figure above, we can see that even if the page exits and the reference between VC and View is disconnected, the reference to runloop-timer-view will still exist, which will still cause the View to be unable to be released, resulting in VC being unable to be released, and thus causing the problem of memory leakage.

There are two cases to be excluded here. The first case is that when the Timer is a non-repeating Timer, [Timer invalidate] can be set at a specific time and the Timer can be set to nil, then there will be no problem. In the second case, when the VC exits, the View is notified to close one by one and the timer is removed, which can also avoid problems.

Why does the runloop-timer-view reference still exist

1. Talk about AFNetworking thread survival. As mentioned in this article, adding a Timer to the child thread RunLoop keeps the child thread alive; At the same time, since the main thread always exists and keeps alive with App, the strong reference of RunLoop to Timer exists no matter whether the Timer runs in the child thread or the main thread.

A strong reference to the timer-view is added to form the complete reference chain of the Runloop-timer-view.

How to break circular references

There are two ways to break circular references:

  • Avoid circular references;
  • Break the circular reference when appropriate.

The two exclusion cases of circular references mentioned earlier are a good example of how to break circular references when appropriate.

Avoid circular references

Since the Timer function is affected by RunLoop, the only possible place to avoid circular references is to break the strong reference chain between the Timer and the View.

There’s nothing that an intermediate object can’t do, and if there is, then add another intermediate object, okay

Here, we can use an intermediate object to disconnect the strong reference between Timer and View, as shown below:

This can be done by making the Timer strongly reference the intermediate object in the manner of the intermediate object, and making the intermediate object weakly reference the Timer and View respectively. The code is as follows:

In the middle of the Timer:

#import <Foundation/Foundation.h> @interface NSTimer (Intermediate) + (NSTimer *)scheduledIntermediateTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; @end ---------------------------- #import "NSTimer+Intermediate.h" #import "TimerIntermediateObject.h" @implementation NSTimer (Intermediate) + (NSTimer *)scheduledIntermediateTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo { TimerIntermediateObject *obj = [[TimerIntermediateObject alloc] init]; obj.target = aTarget; obj.selector = aSelector; // Notice the target and selector here, Is pointing at the intermediate objects and its corresponding methods obj. The timer = [NSTimer scheduledTimerWithTimeInterval: ti target: obj selector: @ the selector (the fire) userInfo:userInfo repeats:yesOrNo]; return obj.timer; } @endCopy the code

Intermediate objects:

#import <Foundation/Foundation.h> @interface TimerIntermediateObject : NSObject @property (nonatomic, weak) NSTimer *timer; @property (nonatomic, weak) id target; @property (nonatomic, assign) SEL selector; - (void)fire:(NSTimer *)timer; @end ---------------------------- #import "TimerIntermediateObject.h" @implementation TimerIntermediateObject - (void)fire:(NSTimer *)timer { if (self.target) { if ([self.target respondsToSelector:self.selector]) { [self.target performSelector:self.selector withObject:timer.userInfo]; }} else {self.timer invalidate; } } @endCopy the code

Note that NSTimer’s classification method is called directly when used

The code analysis

NSTimer’s classification method takes the full set of parameters of the native method and forwards the pointing object and method to the intermediate object.

The weak reference property of the TimerIntermediateObject, an intermediate object, receives a timer and a target, transits the method within the fire: method, takes advantage of the fact that weak objects are automatically set to nil when freed, and calls the corresponding method if target is not nil. Automatically invalidates timer when target is nil.

In this way, Timer and View are separated, and a complete business chain is still built. After the View is released, Timer is automatically invalid, and all resources are released, which perfectly deals with the problem of circular reference.