【问题标题】:IBOutlet crashing with EXC_BAD_ACCESS even though not nilIBOutlet 因 EXC_BAD_ACCESS 而崩溃,即使不是 nil
【发布时间】:2019-05-08 19:13:11
【问题描述】:

在一个 UIViewController (rolePageController) 中,我配置了另一个 UIViewController (drawerController),并从角色页面传递了 2 个 UIView,这将成为抽屉控制器配置的一部分。只要drawerController 尝试从rolePageController 访问IBOutlet 视图,它就会崩溃并出现EXC_BAD_ACCESS(代码=EXC_I386_GPFLT)。

在第一个 VC (rolePageController) 中,这里是 IBOutlets:

@IBOutlet var rolePageDrawerView: UIView!
@IBOutlet var rolePageContentView: UIView!

在 rolePageController.viewDidLoad() 中,我调用了 drawerController.configureDrawer(...):

override func viewDidLoad() {
    super.viewDidLoad()

    //other stuff happens here

    let drawerController = UIStoryboard(name: "StoryboardName", bundle: nil).instantiateViewController(withIdentifier: "drawerController") as! DrawerViewController
    drawerController.configureDrawer(drawerContainerView: self.rolePageDrawerView, overlaidView: self.rolePageContentView)

    //other stuff here
}

DrawerViewController 协议定义为:

protocol DrawerViewController where Self: UIViewController {
    func configureDrawer(drawerContainerView: UIView, overlaidView: UIView)
}

这里是 configureDrawer(...) 函数的代码:

private var drawerParentView: UIView!
private var overlaidByDrawerView: UIView!


func configureDrawer(drawerContainerView: UIView, overlaidView: UIView) {
    self.drawerParentView = drawerContainerView
    self.overlaidByDrawerView = overlaidView
}

在调试器中注意到被调用的drawerController实例与接收调用的self实例不匹配。这是将要调用的实例的地址:

这是我进入通话时实例的地址:

调用前drawerController的地址不是我进入调用时self的地址。那不应该发生。

我创建了一个简化的项目来重现 https://github.com/ksoftllc/DynamicStackBufferOverflow 的崩溃。

解决方案 解决方案原来是从 DrawerViewController 协议中删除 where 子句。

protocol DrawerViewController where Self: UIViewController {
    func configureDrawer(drawerContainerView: UIView, overlaidView: UIView)
}

【问题讨论】:

  • 尝试将内容从 viewDidLoad 移动到 viewDidAppear。听起来它被调用时可能不会在屏幕上绘制。此外,如果某个东西后面有一个!,它就必须在那里,所以它不应该是弱的。不知道为什么 Xcode 开箱即用,但不要隐式展开可选的弱引用。
  • @ChuckKrutsinger 你想分享一个有问题的示例项目吗?您是否尝试重新启动 Xcode、删除应用并重新安装?
  • 是的,一个示例项目会有所帮助。
  • 这不是drawerController的属性的一些无效初始值吗?例如一些旧的故事板连接?
  • 解包隐式解包的可选项(如rolePageDrawerView)不会触发EXC_BAD_ACCESS。在发布版本中,它触发EXC_BAD_INSTRUCTION,在调试版本中,它首先在_swift_runtime_on_report 中停止,然后(如果您继续执行)触发EXC_BAD_INSTRUCTION。所以问题可能不是由于意外的 nil。

标签: ios swift uiviewcontroller exc-bad-access swift-protocols


【解决方案1】:

找到了有问题的代码,但我不知道为什么这会导致我看到的错误。 DrawerController 符合 DrawerViewController 协议,定义为:

protocol DrawerViewController where Self: UIViewController {
    func configureDrawer(drawerContainerView: UIView, overlaidView: UIView)
}

当我删除 Where 条件时,它不再崩溃。

protocol DrawerViewController {
    func configureDrawer(drawerContainerView: UIView, overlaidView: UIView)
}

where 子句对于程序的正确功能实际上并不是必需的,所以我将继续没有它。

更新 我向 swift.org 提交了一个错误并收到了回复。 Swift 4.2 不支持向协议添加 where 子句,但 Swift 5.0 将支持。此外,@J Doe 在下面发布了一种通过更新 Xcode 工具包来实现此目的的方法。

【讨论】:

  • 这可能是一个 Swift 编译器错误。如果你能把它缩减为一个小测试用例,你应该在bugs.swift.org提交一个错误
  • 还在 stackoverflow 上找到了这个答案,它表明在协议中使用 where 子句存在已知问题,如果意图要求某个超类作为协议的一部分,甚至建议一种解决方法。 stackoverflow.com/a/50647762/1256015
  • 仅供参考,我尝试了您的示例项目,它与您在原始问题中描述的略有不同。它没有在configureDrawer 方法内部 崩溃,而是在它之前崩溃。似乎这与 DrawerView 扩展实际上并没有实现所需的方法有关(这已经直接在 DrawerViewController 中完成)。将方法实现移动到协议的实际扩展中导致它在第二行崩溃,类似于您的场景(我不知道为什么不在第一行)。删除 where... 然后再次工作。
  • 更多怪异/提示:直接在vc 变量上调用configureDrawer,无需临时转换为DrawerView 也可以避免崩溃(无论该方法是在扩展中实现还是在直接在课堂上)。这似乎表明它与一些内部铸造有关。我最终也会评论错误报告本身
【解决方案2】:

dynamic-stack-buffer-overflow 与递归没有任何关系。这意味着alloca 缓冲区已溢出。检查asan runtime source code

假设堆栈的布局使得您有一个alloca 缓冲区,后跟一个对象指针——甚至可能是作为参数传递的对象指针之一。

假设alloca 缓冲区溢出。在 asan 构建中,这可能会触发 dynamic-stack-buffer-overflow 错误。但在非 asan 构建中,它只是覆盖该对象指针的字节。假设它写入的字节构成了未映射到进程页表中的地址。

如果程序试图读取该对象指针并将其存储在其他地方(例如,在实例变量中),它必须增加对象的引用计数。但这意味着取消对指针的引用——并且指针指向一个未映射的地址。也许这会导致一般保护错误,Mach calls an EXC_I386_GPFLT

如果您发布了 asan dynamic-stack-buffer-overflow 错误的堆栈跟踪以及导致错误的代码的反汇编,将会很有帮助。

【讨论】:

  • 感谢您对动态堆栈缓冲区溢出的见解。这很有帮助。我很快就会添加堆栈跟踪。
  • 您的堆栈跟踪需要缩进四个空格以便更好地格式化。无论如何,来自print(self) 的堆栈跟踪可能没有用,因为到那时内存损坏已经发生了。需要用asan运行,当asan报错的时候查看stack trace。
  • 有趣的是,print(self) 是内存已经损坏但就在 ASAN 报告它之前的地方。尽管如此,我还是删除了 print(self)。
  • Rob,请看一下堆栈跟踪。我想不通,这是一个表演障碍。
  • 您应该尝试找出正在溢出的缓冲区。每次上栈一帧,然后使用 lldb 命令frame var -L -D1 查看 asan 报告的地址周围是什么。
【解决方案3】:

它看起来真的像 Swift 编译器错误。我简化了您的代码以进行澄清:

func foo(_ wow: TestProtocol) {
    wow.foo()
}

protocol TestProtocol where Self: NSObject {
    func foo()
}

class TestClass: NSObject, TestProtocol {

    func foo() {
        print("Much wow")
    }

}

foo(TestClass())

您可以将此报告为错误。为了解决这个问题,我建议你不要使用 where 语句或传递对象类型为func foo(_ wow: TestClass {

【讨论】:

  • 我昨天将它报告为一个错误,在某处回复某人时提到了它。
  • @ChuckKrutsinger 你可以用我的 Playground 代码替换你的示例。它要简单得多。 :) 这个错误很奇怪,顺便说一句。
  • 错误报告已关闭。他们回应说,在 Swift 4.2 中不支持 where 子句,但在 5.0 中完全实现了
【解决方案4】:

要解决您的问题,请在开发主干工具链快照上运行它。你可以在这里下载:

https://swift.org/download/

转到 Snapshots -> Trunk Development (master) XCode(所以不是 Swift 5.0)并下载截至 12 月 15 日的快照(我从 11 月 30 日获得了快照,但我确定 12 月 15 日也可以。)

安装工具链后,在 XCode 中转到:File -> Preferences -> Components 并选择最新的工具链。 它现在运行没有任何崩溃

此外,Self: UIViewController 的位置可以缩短为 :UIViewcontroller这仅适用于最新的工具链):

protocol DrawerViewController: UIViewController {
    func configureDrawer(drawerContainerView: UIView, overlaidView: UIView)
}

【讨论】:

  • 感谢您的建议。
【解决方案5】:

将此函数调用从 viewDidLoad 移动到 viewWillAppear drawerController.configureDrawer(drawerContainerView: self.rolePageDrawerView, overlaidView: self.rolePageContentView)

【讨论】:

  • 不是一个选项。我正在初始化的东西必须在视图出现之前出现。
  • 有两种不同的方法:viewWillAppear 在视图控制器显示之前调用,viewDidAppear 在视图呈现之后调用。所以你可以在屏幕上显示之前使用viewWillAppear - 但我不知道这在这种情况下是否有帮助。
  • 仍然不是一个选项,原因太复杂,无法在这里解释
  • 这个问题与UIViewControler的生命周期完全无关。 :(
猜你喜欢
  • 2011-03-13
  • 1970-01-01
  • 1970-01-01
  • 2011-04-05
  • 2012-05-10
  • 1970-01-01
  • 2012-02-21
  • 2013-07-16
  • 1970-01-01
相关资源
最近更新 更多