简体   繁体   中英

Swift - Holding closure reference leaks

Why is holding a reference to a closure cause a memory leak even if the closure does not retain anything?

(This started with a SwiftUI issue, but I'm not sure it's related really)

So here's a simple Manager ( ViewModel ) that holds a closure.

class Manager: ObservableObject {
    
    private var handler: (() -> Void)?
    
    deinit {
        print("Manager deallocated")
    }
    
    func shouldDismiss(completion: @escaping () -> Void) {
        handler = completion
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.handler?()
        }
    }
}

Here's a view, using it:

struct LeakingView: View {
    
    @ObservedObject var manager: Manager
    
    let shouldDismiss: () -> Void
    
    var body: some View {
        Button("Dismiss") { [weak manager] in
            manager?.shouldDismiss {
                shouldDismiss()
            }
        }
    }
}

Here's a ViewController activating the flow:

class ViewController: UIViewController {

    var popup: UIViewController?
    
    @IBAction func openViewAction(_ sender: UIButton) {
        
        let manager = Manager()
        
        let suiView = LeakingView(manager: manager) { [weak self] in
            self?.popup?.view.removeFromSuperview()
            self?.popup?.dismiss(animated: true)
            self?.popup = nil
        }
        
        popup = LKHostingView(rootView: suiView)
        
        
        popup?.view.frame.size = CGSize(width: 300, height: 300)
        view.addSubview(popup!.view)
    }
    
}

When running this, as long as the manager is holding the closure, it will leak and the Manager will not get released.
Where is the retain cycle?

Why is holding a reference to a closure cause a memory leak even if the closure does not retain anything?

Because the closure does retain something. It retains self .

For example, when you say

        manager?.shouldDismiss {
            shouldDismiss()
        }

The second shouldDismiss means self.shouldDismiss and retains self . Thus the LeakingView retains the Manager but the Manager, by way of the closure, is now retaining the LeakingView. Retain cycle

This is why people say [weak self] in at the start of such closures.

I would suggest that you might not want to use [weak self] pattern here. You are defining something that should happen ½ second after the view is dismissed. So you probably want to keep that strong reference until the desired action is complete.

One approach is to make sure that Manager simply removes its strong reference after the handler is called:

class Manager: ObservableObject {
    private var handler: (() -> Void)?
    
    func shouldDismiss(completion: @escaping () -> Void) {
        handler = completion
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.handler?()
            self.handler = nil
        }
    }
}

Alternatively, if the use of this completion handler is really limited to this method, you can simplify this to:

class Manager: ObservableObject {
    func shouldDismiss(completion: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            completion()
        }
    }
}

Or

func shouldDismiss(completion: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: completion)
}

All of these approaches will make sure that Manager does not keep any strong references beyond the necessary time.

Now, the caller can choose to use [weak self] if it wants (ie, if it doesn't want the captured references to even survive ½ second), but if you really want the “do such-and-such after it is dismissed”, it probably wouldn't use weak references either. But either way, it should be up to the caller, not the manager. The manager should just ensure that it doesn't hang on to the closure beyond the point that it is no longer needed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM