Hours of light
My blog

1. The cause of the circular reference generated by Timer

Prior to iOS 10, using a Timer would cause the Controller holding the Timer to be unable to release due to circular references, resulting in memory leaks. This has been optimized since iOS 10. The usual code for using Timer before iOS 10 is as follows:

var time: Timer?

override func viewDidLoad(a) {
     super.viewDidLoad()
     time = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(timePrint), userInfo: nil, repeats: true)}func timePrint(a) {
    // do something...
}

deinit {
    print("deinit---------------------11111")
    time?.invalidate()
    time = nil
}
Copy the code

When I use the Timer in the Controller, after the Controller is pop or dismiss, its memory will not be released, you can see that the Timer is also running normally, so what is the reason for this?

1.1. Not just strong references

In general, a strong reference between two instance objects creates a circular reference, so it is understood that the reference relationship between Timer and Controller might look like this:

For this kind of circular reference caused by strong references between the two, just make one of them weak reference can solve the problem, so try it.

Change time to a weak reference

weak var time: Timer?
Copy the code

Run the code again, and the ideal reference relationship between them should look like the following. When the Controller is freed, the Timer’s memory will also be freed because of the weak reference relationship:

When I run the code again, I find that it is not as I expected. When the Controller is freed, the Timer still works, so their memory is still not freed effectively. Why did I use a weak reference and its memory was not freed?

1.2,TimerandRunLoopStrong references between

One problem that is ignored here is the relationship between Timer and RunLoop. When a Timer is created, it is strongly referenced by the current thread’s RunLoop. If the object is created in the main thread, the main thread holds the Timer. When I use weak references, the reference relationship between them is:

Although weak references are used, because the RunLoop in the main thread is resident memory and strongly references the Timer, the Timer also strongly references the Controller, which is indirectly strongly referenced by the RunLoop. Even if the Controller is pop or dismiss, due to strong references, this part of memory cannot be released normally, which will cause memory leakage and may cause the whole App Crash. When controllers are popped or dismissed, their references in memory are:

As for the reason why Timer uses Tatget mode to generate cyclic reference, some blogs found in China believe that the reason is that ViewController, Timer and Tatget form a mutually strong reference closed loop. However, I have read the official document Using Timers: After References to Timers and Object Lifetimes, I do not agree with this view. Of course, if you have other views, please point out and explain the reasons. The official document reads:

Because the run loop maintains the timer, From the perspective of object lifetimes there's no need to keep a reference to a timer after you've scheduled it. (Because the timer is passed as an argument when you specify its method as a selector, you can invalidate a repeating timer when appropriate within that method.) In many situations, however, You also want the option of invalidating the timer -- perhaps even before it starts. In this case, you do need to keep a reference to the timer, so that you can stop it whenever appropriate. If you create an unscheduled timer (see Unscheduled Timers), then you must maintain a strong reference to the timer so that it is not deallocated before you use it.

A timer maintains a strong reference to its target. This means that as long as a timer remains valid, its target will not be deallocated. As a corollary, This means that it does not make sense for a timer's target to try to invalidate the timer in its dealloc method -- the dealloc method will not be invoked as long as the timer is valid.

2、How to solve it ?

Knowing the cause of Timer’s circular reference, what can be done to solve the problem?

2.1 use the Block method provided by the system

After iOS 10, the system has optimized this problem. If it is released after iOS 10, you can use the Block callback method provided by the system:

if #available(iOS 10.0.*) {
      /// iOS 10 uses' Block 'mode to solve the Timer loop reference problem
      time = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { [weak self] (timer) in
            guard let `self` = self else { return }
            self.timePrint()
      })
}
Copy the code

In fact, it is now possible to develop a new App without considering the following compatibility processing of iOS 10, because Apple official statistics: Apple Developer: iOS and iPadOS Usage. As of April 10, 2021, only **8%** of iPhone users are still using iOS 13 or later. Even wechat, an App with hundreds of millions of users, only supports iOS 11 or later. Of course, for older apps that support pre-ios 10, or if you have a demanding product manager, you can only do compatibility with older apps.

2.2. Use those provided by CCPDispatchSourcereplaceTimer
var source: DispatchSourceTimer?

source = DispatchSource.makeTimerSource(flags: [], queue: .global())
source.schedule(deadline: .now(), repeating: 2)
source.setEventHandler {
    // do something...
}
source.resume()

deinit {
     source?.cancel()
     source = nil
}
Copy the code
2.3. Imitate what the system providesclosure

Extend system Timer:

extension Timer {
    class func rp_scheduledTimer(timeInterval ti: TimeInterval.repeats yesOrNo: Bool.closure: @escaping (Timer) - >Void) - >Timer {
        return self.scheduledTimer(timeInterval: ti, target: self, selector: #selector(RP_TimerHandle(timer:)), userInfo: closure, repeats: yesOrNo)
}
    
    @objc class func RP_TimerHandle(timer: Timer) {
        var handleClosure = { }
        handleClosure = timer.userInfo as! () -> ()
        handleClosure()
    }
}
Copy the code

Call method:

if #available(iOS 10.0.*) {
      /// iOS 10 uses' Block 'mode to solve the Timer loop reference problem
      time = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { [weak self] (timer) in
            guard let `self` = self else { return }
            self.timePrint()
      })
} else {
      /// The pre-ios 10 solution was to mimic the system's 'closure' to fix the Timer loop reference problem
      time = Timer.rp_scheduledTimer(timeInterval: 2, repeats: true, closure: { [weak self] (timer) in
            guard let `self` = self else { return }
            self.timePrint()
      })
}
        
func timePrint(a) {
     // de something...
}
    
deinit {
    time?.invalidate()
    time = nil 
}
Copy the code
2.4. Other solutions
  • useRuntimeMethod to add message handling to an object
  • useNSProxyClass as an intermediate object

This paper mainly analyzes the reasons of using Timer in the development of circular reference and some common solutions.