Search code examples
swiftexc-bad-accesscore-motionnszombies

What is the cause of the zombies in the following code


I have the following class for collecting device motion data:

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
        }

}

However I'm getting EXC_BAD_ACCESS after it runs for a while. I ran the zombie instrument and it appears that handleMotionUpdate() is the caller at fault. And MotionDataRecord or some of it's properties are what are being deallocated somehow...

MotionDataRecord is a 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?
}

Any ideas what's going on here?

Edit:

Have added a stripped down version of the project to github here

Edit:

Screenshot of zombies instrument:

zombies instrument screenshot


Solution

  • Okay, I'm going to try to do a little thought-experiment to suggest what might be happening here.

    Keep in mind first the following points:

    • Your MotionDataRecord is a struct consisting almost entirely of reference type instance properties. This forces the struct to participate in reference counting.

    • You are wildly accessing the properties of this struct on different threads. Your locationManager:didUpdateLocations: sets motionDataRecord.location on the main thread, while e.g. your motionManager.startDeviceMotionUpdates sets motionDataRecord.deviceMotion on a background thread (queue).

    • Every time you set a struct property, you mutate the struct. But there is actually no such thing as struct mutation in Swift: a struct is a value type. What really happens is that the entire struct is copied and replaced (initializeBufferWithCopyOfBuffer in the zombie log).

    Okay, so on multiple simultaneous threads you are coming in and replacing your struct-full-of-references. Each time you do that, one struct copy goes out of existence and another comes into existence. It's a struct-full-of-references, so this involves reference counting.

    So suppose the process looks like this:

    1. Make the new struct.

    2. Set the new struct's reference properties to the old struct's reference properties (except for the one we are changing) by copying the references. There is some retain-and-release here but it all balances out.

    3. Set the new struct's reference property that we are replacing. This does a retain on the new value and releases the old value.

    4. Swap the new struct into place.

    But none of that is atomic. Thus, those steps can run out of order, interleaved between one another, because (remember) you've got more than one thread accessing the struct at the same time. So imagine that, on another thread, we access the struct between steps and 3 and 4. In particular, between steps 3 and 4 on one thread, we perform steps 1 and 2 on the other thread. At that moment, the old struct is still in place, with its reference to the property that we are replacing pointing to garbage (because it was released and deallocated in step 3 on the first thread). We attempt to do our copy on the garbage property. Crash.

    So, in a nutshell, I would suggest (1) make MotionDataRecord a class instead of a struct, and (2) get your threading straightened out (at the very least, get onto the main thread in the CMMotionManager callbacks before you touch the MotionDataRecord).