Fast 3-protocol oriented programming leads to random SIGBUS failures
I am in charge of a complete Swift 3 application and one of the crashes that happens regularly is a signal SIGBUS
that I cannot 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 this is the logic of reusing views in the scrollview as described by Apple in the WWDC video (cannot find year and video ...):
PageView is a class that implements ReusableView and is 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 like this:
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 the view that manages the view that is inserted or reused. It is implemented depending on the content 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 DocumentViewerViewController.reuseOrInsertPages()
, initiated by the delegate scrollviewDidScroll
.
What could trigger my signal SIGBUS
here? Is this the default implementation func prepareForReuse() {}
used to make the protocol function optional? Any other ideas?
Of course, this accident is completely random and I have not been able to reproduce it. I am just getting crash reports about this from the prod version of the app. Thank you for your help!
source to share
For me it looks like something went wrong in PageView.prepareForReuse (). I don't know the properties, but from the prepareForReuse function it looks like you are accessing properties that @IBOutlets might be:
bridge = nil imageView.image = nil displaying = nil pageNumber = 0 zoomView.prepareForReuse()
Could it be that imageView
or zoomView
not when you are trying to access them? If so, this might be the most simplistic solution:
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 assuming this because it looks like you are instantiating from Nib and so I am assuming that you are using eg @IBOutlet weak var imageView: UIImageView!
.
If for any reason this image is null, accessing it will crash your application.
source to share