3

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 !

Dean
  • 1,512
  • 13
  • 28

1 Answers1

2

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.

xxtesaxx
  • 6,175
  • 2
  • 31
  • 50
  • Indeed, imageView and zoomView are IBOutlets with a force unwrap, since a few versions of the app. But this crash occurred with a previous version of the app where views weren't IBOutlets. Plus : if it's, indeed, nil ; shouln'd it be a SIGTRAP instead ? Anyway, I'm going to add those safe unwrap in the code and see what happen next. – Dean Jun 06 '17 at 07:38
  • Good question. According to this question https://stackoverflow.com/questions/7446655/exception-types-in-ios-crash-logs SIGBUS means "Access to an invalid memory address." whereas SIGTRAP means "Debugger related" whatever that means. :/ You should definitely check this question and all the answers there – xxtesaxx Jun 06 '17 at 19:32
  • Although I don't know if my problem is completely fixed, you totally deserve the complete bounty for this answer. I'll come back later to make you know if the crash disappeared from our prod logs! – Dean Jun 08 '17 at 08:29
  • Thanks. If not, feel free the update your question and I'll have another look. I'm always happy when I can help a fellow programmer :) – xxtesaxx Jun 08 '17 at 12:47
  • Thanks. If not, feel free the update your question and I'll have another look. I'm always happy when I can help a fellow programmer :) – xxtesaxx Jun 08 '17 at 12:47
  • Unfortunatly, this fix is not enough. I just received a crash from my new version containing your suggestion. – Dean Jun 08 '17 at 15:05
  • Then we need to further investigate. Is it the exact same crash? Can you update your question with more code samples? – xxtesaxx Jun 08 '17 at 15:24
  • Yes it's the exact same crash. What edits or code sample do you think you need to get more informations about what wrong in my code ? – Dean Jun 12 '17 at 11:54
  • The implementation of PageView would be very useful – xxtesaxx Jun 12 '17 at 12:04
  • I'm not sure that relevant. I've tried to remove the "default implementation" of the protocol, witch is not recommended by Apple and was known to cause SIGBUS in iOS 7. I hope this is the solution :) – Dean Jun 22 '17 at 10:05
  • Well, it's still happening. I may have a data race somewhere in my implementation. I will try to investigate that when I will upgrade my MacOS and Xcode in fall. – Dean Jul 06 '17 at 16:37
  • hmm thats really strange. Do you think that maybe theres a problem "under the hood" meaning a failure in the swift language itself? – xxtesaxx Jul 06 '17 at 18:50
  • No, because it's only happening with my PageView, and it's not the only one that use the protocol and that use "prepareForReuse". I think there is something to understand with my actual implementation. As I say, I really suspect a data race somewhere. I will keep this question up to date if I find something interesting. Of course a SIGBUS with swift is not usual. That's why it's interesting understanding what is happening ! – Dean Jul 07 '17 at 07:49
  • I'd love to help if I could but I had to look at the whole code to get a better understanding. Well sometimes its just two statements which have to be executed in reverse order to fix a problem like this. I wish you good luck finding the issue – xxtesaxx Jul 07 '17 at 13:15
  • Now you have the whole PageView implementation in the initial question. Enjoy ! – Dean Jul 07 '17 at 14:03
  • Hmm I just can't find a obvious problem in the code. :( Does the app crash on a particular iOS version or device or does it happen on multiple systems? – xxtesaxx Jul 07 '17 at 22:00