I am experiencing a leak with unowned self under conditions where, to the best of my knowledge, there shouldn't be a leak. Let me show an example, it is a little contrived, so bear with me, I've tried to make the simplest case I could.
Suppose I have a simple view controller that executes a closure on viewDidLoad:
class ViewController2: UIViewController {
var onDidLoad: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onDidLoad?()
}
}
and a class, ViewHandler, that owns an instance of this view controller and injects a call to a notify function into its closure, using an unowned reference:
class ViewHandler {
private let viewController2 = ViewController2()
func getViewController() -> ViewController2 {
viewController2.onDidLoad = { [unowned self] in
self.notify()
}
return viewController2
}
func notify() {
print("My viewcontroller has loaded its view!")
}
}
Then, when its view controller is presented by another view controller, the ViewHandler is leaking when nilled out:
class ViewController: UIViewController {
private var viewHandler: ViewHandler?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewHandler = ViewHandler()
self.present(viewHandler!.getViewController(), animated: true, completion: nil)
viewHandler = nil // ViewHandler is leaking here.
}
}
I know the example may seem a little contrived, but as far as I know there shouldn't be a leak. Let my try and break it down:
Before presenting ViewHandler.ViewController2, ownership should look like this:
ViewController -> ViewHandler -> ViewController2 -|
^ |
|_ _ _ _ unowned _ _ _ _ _ |
After presenting ViewHandler.ViewController2, ownership should look like this:
_______________________________
| v
ViewController -> ViewHandler -> ViewController2 -|
^ |
|_ _ _ _ unowned _ _ _ _ _ |
After nilling out ViewHandler, ownership should look like this:
_______________________________
| v
ViewController ViewHandler -> ViewController2 -|
^ |
|_ _ _ _ unowned _ _ _ _ _ |
Nothing is owning ViewHandler and it should be released. However this is not the case and ViewHandler is leaking.
If I change the reference in the capture list of the closure injected into onDidLoad to weak, there is no leak and ViewHandler is released as expected:
func getViewController() -> ViewController2 {
viewController2.onDidLoad = { [weak self] in
self?.notify()
}
return viewController2
}
Also, something I can't explain, if I keep the reference as unowned and make ViewHandler inherit from NSObject, ViewHandler is released as expected and there is no leak:
class ViewHandler: NSObject {
private let viewController2 = ViewController2()
func getViewController() -> ViewController2 {
viewController2.onDidLoad = { [unowned self] in
self.notify()
}
return viewController2
}
....
}
Anyone who can explain what going on?
According to my current understanding, the NSObject which conforms to NSObjectProtocol. This kind of objects was bridged from Objective-C which has a mature memory management. And when you use
class
, most of us still using this kind of class. It should not hurt if you are build a class from NSObject.The management of
swift class
seems working with a little experimental style as people prefer using astructure
whenever possible. So it's not strange there is some behaviors unexpected.So when you choose
swift class
, you have to think more according to this experience. But the good side is they may bring some new and stable features which is different from classic NSObject.To make it simple, just remove vc2 as a private variable.
In this case, the leak still exists. It's a hard condition to judge with
unowned
. Actually, the ownership of viewHandler has transferred to vc2.When vc2 is released, the leaks are also gone. It's kind of temporary leak.
Even specific, the ownership is occupied by
vc.onDidLoad
. Ifeither
or
So you are suppose to handle here. And thus the "leaking" problem has been solved.