在deinit

时间:2019-07-10 07:15:02

标签: swift uikit

我在Swift中遇到了与懒惰变量有关的崩溃。崩溃的原因很容易理解,但是我不知道一种防止这种崩溃的好方法,而又不会失去使用惰性var所获得的好处。

我有一个类,在使用服务时会懒惰地创建服务的实例。如果服务实例已启动,则必须将其停止,但不一定每次都启动。

class MyClass {
   lazy var service: MyService = {
      // To init and configure this service,
      // we need to reference `self`.
      let service = MyService(key: self.key) // Just pretend key exists :)
      service.delegate = self
      return service
   }

   func thisGetsCalledSometimes() {
      // Calling this function causes the lazy var to
      // get initialised.
      self.service.start()
   }

   deinit {
      // If `thisGetsCalledSometimes` was NOT called,
      // this crashes because the initialising closure
      // for `service` references `self`.
      self.service.stop()
   }
}

我该如何避免这种崩溃,最好是在保留惰性变量的同时又不引入太多新的维护?


编辑:

我无法表示在操场上发生的崩溃,但是当我将这种情况构建到视图控制器中时可以。要重现,请使用单个视图控制器模板创建一个新的Xcode项目,然后将ViewController.swift中的代码替换为以下内容:

import UIKit

// Stuff to create a view stack:

class ViewController: UINavigationController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let firstController = FirstController()
        let navigationController = UINavigationController(rootViewController: firstController)
        self.present(navigationController, animated: false, completion: nil)
    }
}

class FirstController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let button = UIButton()
        button.setTitle("Next screen", for: .normal)
        button.addTarget(self, action: #selector(onNextScreen), for: .touchUpInside)
        self.view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
    }

    @objc func onNextScreen() {
        let secondController = SecondController()
        self.navigationController?.pushViewController(secondController, animated: true)
    }
}

// The service and view controller where the crash happens:

protocol ServiceDelegate: class {
    func service(_ service: Service, didReceive value: Int)
}

class Service {
    weak var delegate: ServiceDelegate?

    func start() {
        print("Starting")
        self.delegate?.service(self, didReceive: 0)
    }

    func stop() {
        print("Stopping")
    }
}


class SecondController: UIViewController {
    private lazy var service: Service = {
        let service = Service()
        service.delegate = self
        return service
    }()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
//        service.start() // <- Comment/uncomment to toggle crash 
    }

    deinit {
        self.service.stop()
    }
}

extension SecondController: ServiceDelegate {
    func service(_ service: Service, didReceive value: Int) {
        print("Value: \(value)")
    }
}

应用启动后,它将显示带有“下一个屏幕”按钮的视图控制器。点击此按钮会将另一个视图控制器推到导航堆栈。点击导航栏中的后退按钮将重现该问题:

  • 如果service.start()(位于viewWillAppear中)未注释,则服务将被初始化,并且在轻击后退按钮时在deinit期间不会发生崩溃。
  • 如果将service.start()注释掉,则在取消初始化之前不会初始化服务。然后,当点击后退按钮时,该应用程序将在service.delegate = self行崩溃。

在最小的示例中,崩溃会产生以下错误,我在实际应用中未看到该错误:

  

objc [88348]:无法形成对TestDeinitWithLazyVar.SecondController类的实例(0x7facade14650)的弱引用。该对象可能被释放过多,或者正在释放。

Screenshot of crash

有趣的是,崩溃仅在涉及UIKit时发生,但我认为游乐场示例仍然指出了问题:我想避免在deinit期间初始化惰性变量。正如@Martin R指出的那样,有了该问题说明,this flag-based solution就足够了。

现在,我想知道为什么它会因视图控制器而崩溃!


编辑2:

似乎不是UIKit导致场景崩溃,而是使用NSObject派生的类。这是在Playground中导致崩溃的最小示例:

import Foundation

protocol MyServiceDelegate: class {}

class MyService {
    weak var delegate: MyServiceDelegate?
    func stop() {}
}

class MyClass: NSObject, MyServiceDelegate {
    lazy var service: MyService = {
        let service = MyService()
        service.delegate = self
        return service
    }()

    deinit {
        print("Deiniting...")
        self.service.stop()
    }
}

func test() {
    let myClass = MyClass()
}

test()

2019年7月19日更新:

我刚遇到this proposal for property wrappers in Swift,它将为问题提供一些优雅的解决方案。例如,我们可以扩展惰性属性包装器以提供值(如果已初始化),否则返回nil(注意:未测试代码):

extension Lazy<T> {
   var ifInitialised: T? {
      guard case . initialized(let value) = self else { return nil }
      return value
   }
}

那我们可以简单地做

deinit {
   self.service.ifInitialised?.stop()
}

1 个答案:

答案 0 :(得分:1)

我只是根据您的讲话而创建的:

protocol Hello {
    func thisGetsCalledSometimes()
}

class MyService {

    var delegate: Hello?

    init(key: String) {
        debugPrint("Init")
    }

    func start() {
        debugPrint("Service Started")
    }

    func stop() {
        debugPrint("Service Stopped")
    }
}

class MyClass: Hello {

    lazy var service: MyService = {
        // To init and configure this service,
        // we need to reference `self`.
        let service = MyService(key: "") // Just pretend key exists :)
        service.delegate = self
        return service
    }()

    func thisGetsCalledSometimes() {
        // Calling this function causes the lazy var to
        // get initialised.
        self.service.start()
    }

    deinit {
        // If `thisGetsCalledSometimes` was NOT called,
        // this crashes because the initialising closure
        // for `service` references `self`.
        self.service.stop()
    }
}

我这样访问:var myService: MyClass? = MyClass(),得到以下输出:

"Init"
"Service Stopped"

您正在寻找什么吗?

  

更新:

这是我根据标记的answer编辑了您的课程。

import UIKit

// Stuff to create a view stack:

class ViewController: UINavigationController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let firstController = FirstController()
        let navigationController = UINavigationController(rootViewController: firstController)
        self.present(navigationController, animated: false, completion: nil)
    }
}

class FirstController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let button = UIButton()
        button.setTitle("Next screen", for: .normal)
        button.addTarget(self, action: #selector(onNextScreen), for: .touchUpInside)
        self.view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
    }

    @objc func onNextScreen() {
        let secondController = SecondController()
        self.navigationController?.pushViewController(secondController, animated: true)
    }
}

// The service and view controller where the crash happens:

protocol ServiceDelegate: class {
    func service(_ service: Service, didReceive value: Int)
}

class Service {
    weak var delegate: ServiceDelegate?

    func start() {
        print("Starting")
        self.delegate?.service(self, didReceive: 0)
    }

    func stop() {
        print("Stopping")
    }

    deinit {
        delegate = nil
    }
}

class SecondController: UIViewController {

    private var isServiceAvailable: Bool = false

    private lazy var service: Service = {
        let service = Service()
        service.delegate = self
        //Make the service available
        self.isServiceAvailable = true
        return service
    }()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
//                service.start() // <- Comment/uncomment to toggle crash
    }

    deinit {
        if self.isServiceAvailable {
            self.service.stop()
        }
    }
}

extension SecondController: ServiceDelegate {
    func service(_ service: Service, didReceive value: Int) {
        print("Value: \(value)")
    }
}

我认为这是唯一的选择!让我知道您是否发现有趣的事情。