我负责一个完整的 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 版本收到有关它的崩溃报告。谢谢你的帮助 !