Избегайте сбоев, когда ленивый var ссылается на себя при первом обращении к deinit - PullRequest
0 голосов
/ 10 июля 2019

У меня сбой, связанный с ленивыми переменами в 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()
   }
}

Как мне избежать этого сбоя, желательно, сохраняя ленивый var, и не вводя слишком много нового обслуживания?


EDIT:

Я не мог представить аварию на детской площадке, но смог, когда встроил этот сценарий в контроллер вида. Для воспроизведения создайте новый проект 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() закомментирован, сервис не инициализируется до deinit. Затем при нажатии кнопки «назад» приложение вылетает на линии service.delegate = self.

В минимальном примере сбой вызывает следующую ошибку, которую я не видел в моем реальном приложении:

objc [88348]: Невозможно сформировать слабую ссылку на экземпляр (0x7facade14650) класса TestDeinitWithLazyVar.SecondController. Возможно, этот объект был чрезмерно освобожден или находится в процессе освобождения.

Screenshot of crash

Интересно, что сбой происходит только тогда, когда задействован UIKit, но я думаю, что пример игровой площадки все еще указывает на проблему: я хотел бы избежать инициализации переменной lazy во время deinit. С этой спецификацией проблемы, как указал @Martin R, этого основанного на флаге решения должно быть достаточно.

Теперь мне интересно, почему он падает с контроллером вида!


РЕДАКТИРОВАТЬ 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()

ОБНОВЛЕНИЕ 19 июля 2019 года:

Я только что натолкнулся на это предложение об оболочках свойств в 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 Ответ

1 голос
/ 10 июля 2019

Я только что создал то, что вы сказали, как показано ниже:

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"

Это то, что вы ищете?

Обновление:

Вот я отредактировал ваш класс на основе помеченного ответа .

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)")
    }
}

Это единственный вариант, я думаю! Дайте мне знать, если найдете что-нибудь интересное.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...