为什么我会使用unowned self?

16

在 iOS 应用程序中经常出现以下模式:

class MyViewController: UIViewController {
    let myModel = MyModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        myModel.foo() { [***] in
            // use self here
        }
    }
}

class MyModel {
    public func foo(complete: () -> Void) {
        // do something
        complete()
    }
}

共识是使用[unowned self][weak self]代替[***],当您可以保证在完成时self不会为nil时,使用unowned,否则使用weak,因为您不能确定引用是否仍然有效。
我不明白的是,为什么我要冒着使用unowned的风险,也许现在我确定该引用永远不会为nil,但这可能会在将来改变。我也可能忽略了一些边缘情况,误解发生。我同样可以始终使用weak,在闭包顶部放置一个guard以便能够使用self而不需要!或?。
unowned有什么用处呢?它比weak + guard更快吗?它是语法糖吗?它似乎违背了Swift保护开发人员免受可能导致崩溃的常见错误的哲学。


1
我不太了解Swift,但在其他编程语言中,像[unowned]这样的概念对于与缺乏引用计数的C等其他语言进行接口交互非常重要。虽然我不知道为什么在你所提供的环境中会使用它。 - Cort Ammon
4个回答

17
unownedweak相比具有轻微的性能优势,因为运行时不必跟踪引用来将其转换为nil当对象消失时。
关于保留周期(强引用循环),无论是weak还是unowned都不会创建强引用(在ARC之前的术语中,都不会增加保留计数),因此不存在引用循环的危险,事实上,这就是为什么你需要在闭包中指定weakunowned用于self的原因。
此外,使用unowned可以将引用用作非可选项,因此您不必在闭包中放置任何解包代码。
我总是使用weak,除非有一个非常好的性能原因不这样做。
请注意,在您的代码中,我认为两者都不必要,因为该闭包不会逃逸,即在函数调用foo结束作用域后,对它的引用不会持续存在。

@JonSnow unowned 也不是强引用。 - JeremyP
“因为运行时不必跟踪引用并在对象消失时将其转换为nil” - 这实际上不是Swift运行时中弱引用的工作方式(尽管在Obj-C运行时中确实如此)。基本上,当对对象形成弱引用时,该对象会获得一个“侧表”,用于跟踪其引用计数(并保留指向对象的指针)。然后,弱引用指向这个侧表。即使对象被释放,弱引用仍然指向该侧表... - Hamish
加载弱引用时,会检查强引用计数;如果为零,则返回nil。然后当弱引用计数达到零时,侧表将被释放。这篇博客文章是关于这个主题的好读物:https://mikeash.com/pyblog/friday-qa-2017-09-22-swift-4-weak-references.html。尽管如此,由于`weak`引用需要额外的解引用(从侧表到对象),因此`unowned`引用仍然可能具有较小的性能优势。 - Hamish
@Hamish 谢谢你,那很有启发性。我认为我的陈述仍然是正确的。运行时仍然通过保持弱引用计数来跟踪它们,并且由于通过侧表进行额外间接访问而导致了性能损失。 - JeremyP
@JeremyP 运行时确实为它们保留了引用计数,但是与 Obj-C 不同的是,它不跟踪弱变量本身的地址,因此不能在强引用计数达到零时立即将它们清零(“在对象消失时将其转换为nil”)。另外,我甚至认为运行时也不会懒惰地清空弱引用(从中加载);它只是等待引用被销毁。 - Hamish
显示剩余2条评论

1
我认为使用unowned比使用weak self增加了风险。错误确实会发生,其中一个例子是在视图控制器中启动API调用,让它等待响应的到来,然后突然弹出该视图控制器可能导致它被释放(如果没有强引用)。当响应到达时,我们的视图控制器对象将不复存在,我们的应用程序将崩溃。我们需要决定在哪个地方使用哪个选项。
正如Jeremy所指出的,unowned不必跟踪引用计数,因此它比强引用和弱引用稍微具有一些性能优势。

0

使用无主引用(unowned reference)在导航控制器中的简单示例:

class RegisterViewController: UIViewController {
    lazy var navigatinRightBarItemsCount: Int = { [unowned self] in
        guard let navigationController = self.navigationController else { return 0 }
        guard let topItem = navigationController.navigationBar.topItem else { return 0 }
        guard let rightBarButtonItems = topItem.rightBarButtonItems else { return 0 }
        return rightBarButtonItems.count
        }()

    override func viewWillAppear(_ animated: Bool) {
        print("Navigation bar right side items count: ", navigationRightBarItemsCount)
    }
}

这意味着必须初始化RegisterViewController,然后我们才能安全地获取self. navigationController,并从此处获取项目计数。如果尝试将其设置为[weak self],XCode会抱怨,因为self(视图控制器)可能为空,所以我们必须使用unowned。

另一个例子是UIBarButtonItem

lazy final private var filterNavigationBarItem: UIBarButtonItem = { [unowned self] in
    return UIBarButtonItem(image: #imageLiteral(resourceName: "filter_icon"),
                           style: .plain,
                           target: self,
                           action: #selector(segueToFilterJobs))
    }()

我们在这里看到了什么?看到的是代表“接收操作消息的对象”的目标,这意味着接收操作消息的对象不能为nil并且必须被初始化。

drewagthis article.中给出了一个非常好的解释。

你真正想要使用[unowned self]或[weak self]的唯一时机是当你会创建强引用循环时。强引用循环是指所有权循环,其中对象最终拥有彼此(可能通过第三方),因此它们永远不会被释放,因为它们都确保对方存在。


0
根据Swit编程指南:自动引用计数
无主引用被期望始终具有值。因此,ARC从不将无主引用的值设置为nil,这意味着无主引用使用非可选类型进行定义。
简而言之,weak是可选类型,而unowned则不是。

但是为什么呢?似乎只使用unowned会带来风险而没有收益。我更愿意保守一些,始终使用weak,并在顶部加上guard self!= nil。 - kevin
3
如果你的程序有一个前提,即对象A 必须不 比对象B存在的时间更长,那么让对象A保持一个unowned而不是weak引用对象B是完全合理的。如果破坏了前提条件,你可能不希望应用程序在无效状态下默默地继续运行; 你想要它终止,这样你就可以修复错误。这不是风险问题,而是确保你拥有一个形式良好的程序的问题。 - Hamish
@Hamish +1 那很有道理。 - badhanganesh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接