Программирование Swift 3 по протоколу приводит к случайным сбоям SIGBUS

Я отвечаю за полное приложение Swift 3, и одно из регулярных сбоев - это SIGBUS сигнал, который я не могу понять вообще:

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

Логика, лежащая в основе этого, - логика повторного использования представлений в виде прокрутки, как описано Apple в видео WWDC (не могу найти год и видео...):

PageView - это класс, который реализует ReusableView и 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
    }
}

где ReusableView и Indexed - протоколы, определенные таким образом:

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 - это представление, которое управляет видом, который вставляется или используется повторно. Это реализовано в зависимости от содержащего типа представления:

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

Ведьма зовется DocumentViewerViewController.reuseOrInsertPages(), вызваны scrollviewDidScroll делегировать.

Что может спровоцировать мой SIGBUS сигнал здесь? Это реализация по умолчанию func prepareForReuse() {} Я использую, чтобы сделать функцию протокола необязательной? Есть другие идеи?

Конечно, этот сбой совершенно случайный, и я не смог его воспроизвести. Я просто получаю отчеты о сбоях из Prod версии приложения. Спасибо за вашу помощь!

1 ответ

Для меня это выглядит так, будто что-то пошло не так в PageView.prepareForReuse(). Я не знаю о свойствах, но из функции prepareForReuse похоже, что вы обращаетесь к свойствам, которые, возможно, являются @IBOutlets:

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

Может ли быть так imageView или же zoomView ноль, когда вы пытаетесь получить к ним доступ? Если это так, это может быть наиболее простым решением:

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

Опять же, я не уверен в деталях реализации вашего PageView, и я только догадываюсь об этом, потому что похоже, что вы создаете его экземпляр из Nib, и поэтому я предполагаю, что вы используете, например, @IBOutlet weak var imageView: UIImageView!,

Если по какой-либо причине этот imageView станет нулевым, доступ к нему приведет к сбою вашего приложения.

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