强引用变量可能会导致内存问题

15

我已经学习使用Swift编程几个月了,最近我更关注于了解Swift语言的工作原理。


因此,在阅读苹果公司有关自动引用计数(ARC)的文档时,我遇到了以下内容:

首先是这段文字:

在大多数情况下,这意味着在 Swift 中内存管理“只管用”,您不需要自己考虑内存管理。 ARC 会在不再需要这些实例时自动释放类实例所使用的内存。

接下来是下一段文字:

为了实现这一点,每当您将类实例分配给属性、常量或变量时,该属性、常量或变量都会对该实例进行强引用。该引用被称为“强引用”,因为它牢牢地保持着该实例,并且只要该强引用存在,就不允许将其释放。


我对这种情况的动态有些困惑。我注意到在使用故事板时,您将引用设置为弱引用,因此类看起来像这样,也就是我所说的情况1:

情况1

class SomeClass : UIViewController {
    @IBOutlet weak var nameLabel : UILabel!

    override func viewDidLoad() {
        nameLabel.text = "something."  
    }  
}

在这里,该标签与 ViewController 之间存在一对一的弱引用关系,只要控制器发生改变,引用就会断开(内存释放),因为它是弱引用。因此,不存在与内存相关的任何问题。

如果上述语句有误或不严谨,请原谅。如果有人能够确认我的假设或者反驳我的假设,我将不胜感激。


然而,我的问题是关于第二种情况,其中我不使用故事板,类似于以下内容:

案例2

class SomeClass : UIViewController {
    var nameLabel : UILabel = {

      let label = UILabel()
      label.translatesAutoresizingMaskIntoConstraints = false
      return label

    }()

    override func viewDidLoad() {
        view.addSubView(nameLabel)
        // view.addConstraints...
    }  
}

针对以上情况,我的假设是ViewController与label之间存在一对一的强引用,而ViewController内部的view也与label保持强引用。如果更改类别/从子视图中删除标签,则我认为内存将不会被释放。或者至少视图控制器将保持对标签的强引用(根据文档)。

我通过从视图的子视图中删除标签并打印出标签来确认这一点(它给了我一个带有0原点和0大小的Frame的UILabel实例),因此是一个不为nil的实例。

我能从中收集到的唯一信息是,虽然标签已从UIView中删除,但它仍然与控制器保持强引用,因此在内存中保持永久状态。我的理解正确吗?

如果这是问题所在,我应该如何防止代码出现此类内存问题?更大的问题是如果我像这样声明变量,在将其作为主视图的子视图添加时,我将得到一个nil。

    weak var nameLabel : UILabel = {

      let label = UILabel()
      label.translatesAutoresizingMaskIntoConstraints = false
      return label

    }()

如果像第二个案例一样声明变量可能会导致永久的强引用,那么我应该如何声明它们才不会出现内存问题?


因此,我的问题是:

在没有使用任何storyboard outlet的情况下,如果将变量强烈引用到视图控制器中,这些引用会导致内存问题吗?

如果是这样,请提供代码声明实践

如果不是这样,请提供有力的论据和有效的解释加以反驳。


如果我有任何错误之处,请谅解。

提前感谢你。


1
最佳实践问题通常不适合在Stack Overflow上提问,因为答案只能基于观点而非事实。 - JAL
@JAL 这个问题很具体,要求提供上述问题的最佳解决方案。这个问题并不要求回答者从几个备选方案中选择最佳解决方案。我已经编辑了标题。 - Akshansh Thakur
使用强引用到IBOutlets意味着如果它们从superview中移除,outlets仍将被保留。这是否是一个问题取决于应用程序的要求。如果您有一些视图从层次结构中删除并且您需要保留对其进行后续重用的引用,则应该是强引用。另一方面,如果您希望在从视图中删除时释放视图,则引用应该是弱引用。这与任何其他变量完全相同。 - Luke Van In
6个回答

13
我能理解的是,虽然UIView上的标签被移除了,但它仍与控制器保持强引用,因此在内存中保持永久状态。我的理解正确吗?
不对。这里并没有大问题。
标签没有对视图控制器保持强引用 - 如果有,那就会造成保留循环,并导致标签和视图控制器泄漏。因此,视图应该从来不保持对其视图控制器的强引用。
然而,在这种情况下,情况正好相反:视图控制器对标签保持强引用。这没问题。的确,标签因此会在从其父视图中移除后继续存在。但这可能不是坏事。在许多情况下,这是好的!例如,假设您打算稍后将标签放回界面;你将需要保留它。
如果您确定不需要稍后保留标签,则只需使用一个将UILabel封装为可选项的实例属性。这样,当您完成使用标签时,可以将nil分配给标签实例属性,标签将消失。
但无论如何,这里都没有泄漏,您应该停止担心。当视图控制器消失时,标签也将消失。标签存在的时间比必须要长,但在事情的整个规模上来看,这微不足道且无关紧要。

我已经点赞了。谢谢你的回答!如果你能提供一些阅读材料或资源,那就太好了。谢谢!我很可能会接受这个答案,但是请随意添加更多细节或信息。 - Akshansh Thakur
2
我只是简单地陈述了一些事实。这不像有任何疑问或争议的余地。如果你对内存管理如何工作感到困惑,可以阅读我书中的内存管理部分:https://apeth.com/iOSBook/ch12.html#_memory_management 这一章的免费在线版本是关于Objective-C的,但基本事实与Swift相同。事实上,在这个版本的章节中,我比现代版的书更详细地介绍了ARC的工作原理,因为正如你之前引用的那样,ARC“只是工作”,所以不必担心这些细节。 - matt
我同意。这种情况并不符合内存泄漏的定义,因为没有创建保留循环。UILabel仍然在内存中,因为控制器对它有一个强引用,除非你将另一个标签设置为该属性或将其置为空(这需要将其声明为可选项)。 - markedwardmurray
还有,嗨,Matt Neuburg!我喜欢你的书。你的 Objc 基础知识帮助我开始 iOS :) - markedwardmurray
@matt 非常感谢你。根据你的建议,我自己进行了一些学习,这帮助我学到了许多新的基础知识。 - Akshansh Thakur

1
我认为对视图控制器有强引用的变量不会导致任何内存问题。
通常情况下,视图在销毁其视图控制器之前被释放。例如,在您的代码中,当释放视图时,ARC 减少指向 namelabel 的计数器,因此从 2 变为 1。然后,在释放视图控制器时,它再次减少计数器,从 1 减少到 0。一旦指向 namelabel 的引用计数达到 0,它就被移除了。

1
弱引用是一种不会对所引用的实例保持强引用的引用,因此不会阻止ARC处理所引用的实例。这种行为可以防止引用成为强引用循环的一部分。通过在属性或变量声明前放置weak关键字来表示弱引用。
必须将弱引用声明为变量,以指示它们的值可以在运行时更改。不能将弱引用声明为常量。
由于弱引用不会对所引用的实例保持强引用,因此可能会在弱引用仍然引用它时对其进行清除。因此,当所引用的实例被清除时,ARC会自动将弱引用设置为nil。由于弱引用需要允许nil作为其值,因此它们始终具有可选类型。您可以像处理任何其他可选值一样检查弱引用中是否存在值,并且永远不会得到对不存在的无效实例的引用。

来源: 苹果文档

弱引用只是指向一个对象的指针,它不会通过ARC保护对象免受释放。虽然强引用会将对象的保留计数增加1,但弱引用不会。此外,当成功释放时,弱引用会将指向您对象的指针清零。这确保了当您访问弱引用时,它要么是有效的对象,要么是nil。

希望可以帮助您更好地理解弱引用,无论是与故事板项目相关还是通过编程创建。


1
我总是这样向我的学生解释。
有了强引用,你可以看到一个值,并且你可以像套索一样控制它。你可以决定这个值是否保持活动状态。
有了弱引用,你可以看到这个值,但是没有套索。你无法决定这个值是否存活。

1

针对您的情况,为了避免内存泄漏发生一秒钟,您可以采用Matt的答案。

为了更好地理解,在build phases->Complie sources下创建一个自定义的UILabel类,并在MRC标志下进行设置。

在自定义类中,覆盖retain和release方法。在它们上面放置断点。

在具有ARC标志的视图控制器中使用该自定义UILabel类。选择Matt的答案或使用以下可选的UILabel声明。

import UIKit

class ViewController: UIViewController {
    var label:UILabel? = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "something"
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.label!)
        //namelabel goes out of scope when method exists.
        //self.view has 1+ ref of self.label
    }
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.label?.removeFromSuperview()//-1 ref of self.label
        self.label = nil
        print(self.label)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

你将清楚地了解ARC的工作原理以及为什么将UILabel的弱引用添加到UIView中会导致崩溃。

1

当你需要创建 label 时,调用 addsubView 来对其进行强引用,并对你的成员变量进行弱引用,如下所示:

class ViewController: UIViewController {

weak var label : UILabel?

override func viewDidLoad() {
    super.viewDidLoad()

    let label = UILabel()
    view.addSubview(label)
    self.label = label

}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

    print(label)
    //click first Optional(<UILabel: 0x7fb562c3f260; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x7fb562c11c70>>)
    //click second nil
    label?.removeFromSuperview()
}
}

无论如何,当 viewcontroller 释放时,标签将会被释放,view.subview 也会被释放。

演示

我编写了一个简单的演示,将 ViewControllerTest 设置为 rootviewcontroller

class Test{

weak var label:UILabel?

static let instance = Test()


}



class ViewControllerTest: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()

    let item = UIBarButtonItem(title: "Test", style: .Plain, target: self, action: #selector(self.test))
    self.navigationItem.rightBarButtonItem = item

}

func test(){
    print(Test.instance.label)
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {


    let vc = ViewController()
    self.navigationController?.pushViewController(vc, animated: true)
    print(vc.nameLabel)
    let test = Test.instance
    test.label = vc.nameLabel

}

}



class ViewController: UIViewController {

var nameLabel : UILabel = {

    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    return label

}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = UIColor.whiteColor()
    view.addSubview(nameLabel)

    let item = UIBarButtonItem(title: "Test", style: .Plain, target: self, action: #selector(self.test))
    self.navigationItem.rightBarButtonItem = item

}

func test(){
    print(Test.instance.label)
}
}

嘿,这似乎是一个聪明的解决方法。但是你是否在暗示我需要重新声明在视图控制器中引用的每个变量?这是编码中常见的做法吗?我正在寻找最佳行业实践。 - Akshansh Thakur
@AkshanshThakur 是的,没错。但是你这样做不会导致内存问题,因为当 viewcontroller 释放时,所有两个强引用都会被打破。 - Wilson XJ
好的。你能确认一下吗?有相关的来源或相关文章吗?我确定当视图控制器被释放时,视图将被释放。但是标签也会从视图中释放吗? - Akshansh Thakur
@AkshanshThakur请尝试一下演示。 - Wilson XJ

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