3

我负责一个完整的 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)
                }
            }
        }
    }
}

Witch 由 调用,由委托DocumentViewerViewController.reuseOrInsertPages()触发。scrollviewDidScroll

什么能SIGBUS在这里激起我的信号?func prepareForReuse() {}这是我用来使协议功能可选的默认实现吗?还有其他想法吗?

当然,这次崩溃是完全随机的,我无法重现它。我刚刚从应用程序的 prod 版本收到有关它的崩溃报告。谢谢你的帮助 !

4

1 回答 1

2

对我来说,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 变为 nil,访问它将使您的应用程序崩溃。

于 2017-06-05T19:28:29.170 回答