Что является причиной зомби в следующем коде

У меня есть следующий класс для сбора данных о движении устройства:

class MotionManager: NSObject {
        static let shared = MotionManager()
        private override init() {}

        // MARK: - Class Variables

        private let motionManager = CMMotionManager()

        fileprivate lazy var locationManager: CLLocationManager = {
                var locationManager = CLLocationManager()
                locationManager.delegate = self
                locationManager.desiredAccuracy = kCLLocationAccuracyBest
                locationManager.activityType = .fitness
                locationManager.distanceFilter = 10.0
                return locationManager
        }()

        private let queue: OperationQueue = {
                let queue = OperationQueue()
                queue.name = "MotionQueue"
                queue.qualityOfService = .utility
                return queue
        }()

        fileprivate var motionDataRecord = MotionDataRecord()

        private var attitudeReferenceFrame: CMAttitudeReferenceFrame = .xTrueNorthZVertical

        var interval: TimeInterval = 0.01
        var startTime: TimeInterval?

        // MARK: - Class Functions

        func start() {
                startTime = Date().timeIntervalSince1970
                startDeviceMotion()
                startAccelerometer()
                startGyroscope()
                startMagnetometer()
                startCoreLocation()
        }

        func startCoreLocation() {
                switch CLLocationManager.authorizationStatus() {
                case .authorizedAlways:
                        locationManager.startUpdatingLocation()
                        locationManager.startUpdatingHeading()
                case .notDetermined:
                        locationManager.requestAlwaysAuthorization()
                case .authorizedWhenInUse, .restricted, .denied:
                        break
                }
        }

        func startAccelerometer() {
                if motionManager.isAccelerometerAvailable {
                        motionManager.accelerometerUpdateInterval = interval
                        motionManager.startAccelerometerUpdates(to: queue) { (data, error) in
                                if error != nil {
                                        log.error("Accelerometer Error: \(error!)")
                                }
                                guard let data = data else { return }
                                self.motionDataRecord.accelerometer = data
                        }
                } else {
                        log.error("The accelerometer is not available")
                }

        }

        func startGyroscope() {
                if motionManager.isGyroAvailable {
                        motionManager.gyroUpdateInterval = interval
                        motionManager.startGyroUpdates(to: queue) { (data, error) in
                                if error != nil {
                                        log.error("Gyroscope Error: \(error!)")
                                }
                                guard let data = data else { return }
                                self.motionDataRecord.gyro = data
                        }
                } else {
                        log.error("The gyroscope is not available")
                }
        }

        func startMagnetometer() {
                if motionManager.isMagnetometerAvailable {
                        motionManager.magnetometerUpdateInterval = interval
                        motionManager.startMagnetometerUpdates(to: queue) { (data, error) in
                                if error != nil {
                                        log.error("Magnetometer Error: \(error!)")
                                }
                                guard let data = data else { return }
                                self.motionDataRecord.magnetometer = data
                        }
                } else {
                        log.error("The magnetometer is not available")
                }
        }

        func startDeviceMotion() {
                if motionManager.isDeviceMotionAvailable {
                        motionManager.deviceMotionUpdateInterval = interval
                        motionManager.startDeviceMotionUpdates(using: attitudeReferenceFrame, to: queue) { (data, error) in
                                if error != nil {
                                        log.error("Device Motion Error: \(error!)")
                                }
                                guard let data = data else { return }
                                self.motionDataRecord.deviceMotion = data
                                self.motionDataRecord.timestamp = Date().timeIntervalSince1970
                                self.handleMotionUpdate()
                        }
                } else {
                        log.error("Device motion is not available")
                }
        }

        func stop() {
                locationManager.stopUpdatingLocation()
                locationManager.stopUpdatingHeading()
                motionManager.stopAccelerometerUpdates()
                motionManager.stopGyroUpdates()
                motionManager.stopMagnetometerUpdates()
                motionManager.stopDeviceMotionUpdates()
        }

        func handleMotionUpdate() {
                print(motionDataRecord)
        }

}

// MARK: - Location Manager Delegate
extension MotionManager: CLLocationManagerDelegate {

        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
                if status == .authorizedAlways || status == .authorizedWhenInUse {
                        locationManager.startUpdatingLocation()
                } else {
                        locationManager.stopUpdatingLocation()
                }
        }

        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
                guard let location = locations.last else { return }
                motionDataRecord.location = location
        }

        func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
                motionDataRecord.heading = newHeading
        }

}

Однако я получаю EXC_BAD_ACCESS после того, как он работает некоторое время. Я запустил инструмент зомби, и кажется, что handleMotionUpdate() виноват вызывающий. А также MotionDataRecord или некоторые его свойства как-то освобождаются...

MotionDataRecord это struct:

struct MotionDataRecord {
    var timestamp: TimeInterval = 0
    var location: CLLocation?
    var heading: CLHeading?
    var motionAttitudeReferenceFrame: CMAttitudeReferenceFrame = .xTrueNorthZVertical
    var deviceMotion: CMDeviceMotion?
    var altimeter: CMAltitudeData?
    var accelerometer: CMAccelerometerData?
    var gyro: CMGyroData?
    var magnetometer: CMMagnetometerData?
}

Есть идеи, что здесь происходит?

Редактировать:

Добавили урезанную версию проекта в github здесь

Редактировать:

Скриншот инструмента зомби:

скриншот зомби инструмент

1 ответ

Решение

Хорошо, я попытаюсь провести небольшой мысленный эксперимент, чтобы предположить, что здесь может происходить.

Имейте в виду в первую очередь следующие моменты:

  • Ваш MotionDataRecord - это структура, состоящая почти полностью из свойств экземпляра ссылочного типа. Это заставляет структуру участвовать в подсчете ссылок.

  • Вы дико обращаетесь к свойствам этой структуры в разных потоках. Ваш locationManager:didUpdateLocations: наборы motionDataRecord.location в основной теме, в то время как, например, ваш motionManager.startDeviceMotionUpdates наборы motionDataRecord.deviceMotion в фоновом потоке (queue).

  • Каждый раз, когда вы устанавливаете свойство структуры, вы изменяете структуру. Но на самом деле в Swift нет такой вещи, как структурная мутация: структура является типом значения. Что действительно происходит, так это то, что вся структура копируется и заменяется (initializeBufferWithCopyOfBuffer в журнале зомби).

Итак, в нескольких одновременных потоках вы входите и заменяете свои struct-full-of-links. Каждый раз, когда вы делаете это, одна копия структуры исчезает, а другая появляется. Это структура, полная ссылок, так что это включает в себя подсчет ссылок.

Итак, предположим, что процесс выглядит так:

  1. Создайте новую структуру.

  2. Установите свойства ссылки новой структуры на свойства ссылки старой структуры (кроме той, которую мы меняем), скопировав ссылки. Здесь есть некоторые сохранения и освобождения, но все это уравновешивается.

  3. Установите свойство ссылки новой структуры, которое мы заменяем. Это сохраняет новое значение и освобождает старое значение.

  4. Поменяйте местами новую структуру.

Но ничего из этого не является атомным. Таким образом, эти шаги могут выполняться не по порядку, чередуясь друг с другом, потому что (помните), у вас есть несколько потоков, обращающихся к структуре одновременно. Итак, представьте, что в другом потоке мы обращаемся к структуре между шагами и 3 и 4. В частности, между шагами 3 и 4 в одном потоке мы выполняем шаги 1 и 2 в другом потоке. В этот момент старая структура все еще на месте, и ее ссылка на заменяемое нами свойство указывает на мусор (поскольку он был освобожден и освобожден на шаге 3 в первом потоке). Мы пытаемся сделать нашу копию на свойстве мусора. Краш.

Итак, в двух словах, я хотел бы предложить (1) сделать MotionDataRecord классом вместо структуры, и (2) выправить поток в потоке (по крайней мере, попасть в основной поток в обратных вызовах CMMotionManager, прежде чем вы коснетесь MotionDataRecord).

Другие вопросы по тегам