Swift 3 Protocol Oriented Programming results in random SIGBUS crashes

363 Views Asked by At

I am responsible of a complete Swift 3 application and one of the crashes that occurs regularly is a SIGBUS signal that I can't understand at all:

Thread 0 Crashed:
0   libswiftCore.dylib    0x00000001009b4ac8 0x1007b8000 +2083528
1   LeadingBoards         @objc PageView.prepareForReuse() -> () (in LeadingBoards) (PageView.swift:0) +1114196
2   LeadingBoards         specialized ReusableContentView<A where ...>.reuseOrInsertView(first : Int, last : Int) -> () (in LeadingBoards) (ReusableView.swift:101) +1730152
3   LeadingBoards         DocumentViewerViewController.reuseOrInsertPages() -> () (in LeadingBoards) (DocumentViewerViewController.swift:0) +1036080
4   LeadingBoards         specialized DocumentViewerViewController.scrollViewDidScroll(UIScrollView) -> () (in LeadingBoards) (DocumentViewerViewController.swift:652) +1089744
5   LeadingBoards         @objc DocumentViewerViewController.scrollViewDidScroll(UIScrollView) -> () (in LeadingBoards) +1028252
6   UIKit                 0x000000018c2a68d4 0x18bf85000 +3283156
7   UIKit                 0x000000018bfb2c08 0x18bf85000 +187400
8   UIKit                 0x000000018c143e5c 0x18bf85000 +1830492
9   UIKit                 0x000000018c143b4c 0x18bf85000 +1829708
10  QuartzCore            0x00000001890755dc 0x18906b000 +42460
11  QuartzCore            0x000000018907548c 0x18906b000 +42124
12  IOKit                 0x00000001860d7b9c 0x1860d2000 +23452
13  CoreFoundation        0x0000000185e01960 0x185d3e000 +801120
14  CoreFoundation        0x0000000185e19ae4 0x185d3e000 +899812
15  CoreFoundation        0x0000000185e19284 0x185d3e000 +897668
16  CoreFoundation        0x0000000185e16d98 0x185d3e000 +888216
17  CoreFoundation        0x0000000185d46da4 0x185d3e000 +36260
18  GraphicsServices      0x00000001877b0074 0x1877a4000 +49268
19  UIKit                 0x000000018bffa058 0x18bf85000 +479320
20  LeadingBoards         main (in LeadingBoards) (AppDelegate.swift:13) +77204
21  libdyld.dylib         0x0000000184d5559c 0x184d51000 +17820

The logic behind that is the logic for reusing views in a scrollview, as described by Apple in a WWDC video (can't find the year and the video...):

PageView is a class that implement ReusableView and Indexed:

class PageView: UIView {

    enum Errors: Error {
        case badConfiguration
        case noImage
    }

    enum Resolution: String {
        case high
        case low

        static var emptyGeneratingTracker: [PageView.Resolution: Set<String>] {
            return [.high:Set(),
                    .low:Set()]
        }

        /// SHOULD NOT BE 0
        var quality: CGFloat {
            switch self {
            case .high:
                return 1
            case .low:
                return 0.3
            }
        }

        var JPEGQuality: CGFloat {
            switch self {
            case .high:
                return 0.8
            case .low:
                return 0.25
            }
        }

        var atomicWrite: Bool {
            switch self {
            case .high:
                return false
            case .low:
                return true
            }
        }

        var interpolationQuality: CGInterpolationQuality {
            switch self {
            case .high:
                return .high
            case .low:
                return .low
            }
        }

        var dispatchQueue: OperationQueue {
            switch self {
            case .high:
                return DocumentBridge.highResOperationQueue
            case .low:
                return DocumentBridge.lowResOperationQueue
            }
        }
    }

    @IBOutlet weak var imageView: UIImageView!

    // Loading
    @IBOutlet weak var loadingStackView: UIStackView!
    @IBOutlet weak var pageNumberLabel: UILabel!

    // Error
    @IBOutlet weak var errorStackView: UIStackView!

    // Zoom
    @IBOutlet weak var zoomView: PageZoomView!

    fileprivate weak var bridge: DocumentBridge?

    var displaying: Resolution?
    var pageNumber = 0

    override func layoutSubviews() {
        super.layoutSubviews()

        refreshImageIfNeeded()
    }

    func configure(_ pageNumber: Int, zooming: Bool, bridge: DocumentBridge) throws {
        if pageNumber > 0 && pageNumber <= bridge.numberOfPages {
            self.bridge = bridge
            self.pageNumber = pageNumber
            self.zoomView.configure(bridge: bridge, pageNumber: pageNumber)
        } else {
            throw Errors.badConfiguration
        }

        NotificationCenter.default.addObserver(self, selector: #selector(self.pageRendered(_:)), name: .pageRendered, object: bridge)
        NotificationCenter.default.addObserver(self, selector: #selector(self.pageFailedRendering(_:)), name: .pageFailedRendering, object: bridge)

        pageNumberLabel.text = "PAGE".localized + " \(pageNumber)"

        if displaying == nil {
            loadingStackView.isHidden = false
            errorStackView.isHidden = true
        }
        if displaying != .high {
            refreshImage()
        }

        if zooming {
            startZooming()
        } else {
            stopZooming()
        }
    }

    fileprivate func isNotificationRelated(notification: Notification) -> Bool {
        guard let userInfo = notification.userInfo else {
            return false
        }

        guard pageNumber == userInfo[DocumentBridge.PageNotificationKey.PageNumber.rawValue] as? Int else {
            return false
        }

        guard Int(round(bounds.width)) == userInfo[DocumentBridge.PageNotificationKey.Width.rawValue] as? Int else {
            return false
        }

        guard userInfo[DocumentBridge.PageNotificationKey.Notes.rawValue] as? Bool == false else {
            return false
        }

        return true
    }

    func pageRendered(_ notification: Notification) {
        guard isNotificationRelated(notification: notification) else {
            return
        }

        if displaying == nil || (displaying == .low && notification.userInfo?[DocumentBridge.PageNotificationKey.Resolution.rawValue] as? String == Resolution.high.rawValue) {
            refreshImage()
        }
    }

    func pageFailedRendering(_ notification: Notification) {
        guard isNotificationRelated(notification: notification) else {
            return
        }

        if displaying == nil {
            imageView.image = nil
            loadingStackView.isHidden = true
            errorStackView.isHidden = false
        }
    }

    func refreshImageIfNeeded() {
        if displaying != .high {
            refreshImage()
        }
    }

    fileprivate func refreshImage() {
        let pageNumber = self.pageNumber
        let width = Int(round(bounds.width))
        DispatchQueue.global(qos: .userInitiated).async(execute: { [weak self] () in
            do {
                try self?.setImage(pageNumber, width: width, resolution: .high)
            } catch {
                _ = try? self?.setImage(pageNumber, width: width, resolution: .low)
            }
        })
    }

    func setImage(_ pageNumber: Int, width: Int, resolution: Resolution) throws {
        if let image = try self.bridge?.getImage(page: pageNumber, width: width, resolution: resolution) {
            DispatchQueue.main.async(execute: { [weak self] () in
                if pageNumber == self?.pageNumber {
                    self?.imageView?.image = image
                    self?.displaying = resolution
                    self?.loadingStackView.isHidden = true
                    self?.errorStackView.isHidden = true
                }
            })
        } else {
            throw Errors.noImage
        }
    }
}

extension PageView: ReusableView, Indexed {
    static func instanciate() -> PageView {
        return UINib(nibName: "PageView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! PageView
    }

    var index: Int {
        return pageNumber
    }

    func hasBeenAddedToSuperview() { }

    func willBeRemovedFromSuperview() { }

    func prepareForReuse() {
        NotificationCenter.default.removeObserver(self, name: .pageRendered, object: nil)
        NotificationCenter.default.removeObserver(self, name: .pageFailedRendering, object: nil)

        bridge = nil
        imageView?.image = nil
        displaying = nil
        pageNumber = 0
        zoomView?.prepareForReuse()
    }

    func prepareForRelease() { }
}

// MARK: - Zoom
extension PageView {
    func startZooming() {
        bringSubview(toFront: zoomView)
        zoomView.isHidden = false
        setNeedsDisplay()
    }

    func stopZooming() {
        zoomView.isHidden = true
    }
}

where ReusableView and Indexed are protocols defined that way :

protocol Indexed {
    var index: Int { get }
}

protocol ReusableView {
    associatedtype A

    static func instanciate() -> A

    func hasBeenAddedToSuperview()
    func willBeRemovedFromSuperview()
    func prepareForReuse()
    func prepareForRelease()
}

// Make some func optionals
extension ReusableView {
    func hasBeenAddedToSuperview() {}
    func willBeRemovedFromSuperview() {}
    func prepareForReuse() {}
    func prepareForRelease() {}
}

ReusableContentView is a view that manage the view that are inserted, or reused. It's implemented depending of the containing view type :

class ReusableContentView<T: ReusableView>: UIView where T: UIView {
    var visible = Set<T>()
    var reusable = Set<T>()

    ...
}

extension ReusableContentView where T: Indexed {
    /// To insert view using a range of ids
    func reuseOrInsertView(first: Int, last: Int) {
        // Removing no longer needed views
        for view in visible {
            if view.index < first || view.index > last {
                reusable.insert(view)
                view.willBeRemovedFromSuperview()
                view.removeFromSuperview()
                view.prepareForReuse()
            }
        }
        // Removing reusable pages from visible pages array
        visible.subtract(reusable)

        // Add the missing views
        for index in first...last {
            if !visible.map({ $0.index }).contains(index) {
                let view = dequeueReusableView() ?? T.instanciate() as! T // Getting a new page, dequeued or initialized
                if configureViewWithIndex?(view, index) == true {
                    addSubview(view)
                    view.hasBeenAddedToSuperview()
                    visible.insert(view)
                }
            }
        }
    }
}

Witch is called by DocumentViewerViewController.reuseOrInsertPages(), triggered by scrollviewDidScroll delegate.

What can provoque my SIGBUS signal here? Is that the default implementation of func prepareForReuse() {} I use to make the protocol function optional? Any other ideas?

Of course, this crash is completly random and I wasn't able to reproduice it. I just receive crash reports about it from prod version of the app. Thanks for your help !

1

There are 1 best solutions below

16
On

For me it looks like something went wrong in PageView.prepareForReuse(). I'm not aware of the properties but from the prepareForReuse function it looks like your are accessing properties which maybe are @IBOutlets:

bridge = nil
imageView.image = nil
displaying = nil
pageNumber = 0
zoomView.prepareForReuse()

Could it be that imageView or zoomView are nil when you try to access them? If so, this could be the most simplistic fix:

func prepareForReuse() {
    NotificationCenter.default.removeObserver(self, name: .pageRendered, object: nil)
    NotificationCenter.default.removeObserver(self, name: .pageFailedRendering, object: nil)

    bridge = nil
    imageView?.image = nil
    displaying = nil
    pageNumber = 0
    zoomView?.prepareForReuse()
}

Again, I am not sure about the implementation details of your PageView and I am only guessing this because it looks like you are instantiating it from a Nib and therefore my guess is you are using for example @IBOutlet weak var imageView: UIImageView!.

If for whatever reason this imageView becomes nil, accessing it will crash your app.