我在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)的弱引用。该对象可能被释放过多,或者正在释放。
有趣的是,崩溃仅在涉及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()
}
答案 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)")
}
}
我认为这是唯一的选择!让我知道您是否发现有趣的事情。