【问题标题】:Strongly referenced variable may cause memory issues强引用变量可能会导致内存问题
【发布时间】:2016-11-15 17:02:14
【问题描述】:

我已经在Swift 编程了几个月了。最近,我更加关注Swift 作为一种语言的工作原理。


因此,最近在阅读apple documentation on Automatic Reference Counting(ARC) 时,我遇到了以下几行:

这个在上面:

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

在下一段中,以下内容:

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


我对这种情况的动态有些困惑。我在使用情节提要时注意到,您设置了对弱的引用,因此该类看起来像这样,我也称之为案例 1:

案例 1

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

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

这里,标签与 ViewController 是一对一的弱引用,一旦 Controller 改变,引用就会被破坏(内存释放),因为它是弱的。因此,没有与内存相关的问题。

如果上述陈述是错误的或松散的,请原谅我。如果有人证实或反驳我的假设,我会很高兴。


我的问题是关于第二种情况,我不使用情节提要,类如下所示:

案例 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有强引用。如果class改变/label被移除subview ..那么我认为内存不会被释放。或者至少视图控制器将保持对标签的强引用(根据文档)。

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

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

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

    weak var nameLabel : UILabel = {

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

    }()

如果像第二种情况那样声明变量会导致永久强引用,我应该如何声明它们而不是出现内存问题?


总之,我的问题是:

如果没有使用storyboard outlet,并且变量被强引用到视图控制器,这些引用会导致内存问题吗?

如果是这样,我必须关注什么code declaration practice

如果不是这样,请提供深思熟虑的论据和有效的解释来反驳它。


再次,如果我在任何地方不正确,请原谅我。

提前谢谢你。

【问题讨论】:

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

标签: ios swift memory-management automatic-ref-counting


【解决方案1】:

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

没有。这里没有什么大问题。

标签没有对视图控制器的强引用——如果有,将是一个保留循环,并会导致标签和视图控制器泄漏。出于这个原因,视图应该从不保持对其视图控制器的强引用。

然而,这里的情况正好相反:视图控制器对标签有一个强引用。没关系。确实,标签在从其超级视图中删除后仍然存在。但这可能还不错。在很多情况下,这很好!例如,假设您打算稍后将标签放回界面中;您将需要保留它。

如果您确定以后不需要保留标签,那么只需使用包装 UILabel 的 Optional 作为您的实例属性。这样,您可以在完成后将nil 分配给标签实例属性,标签将不复存在。

但无论如何,这里没有泄漏,你应该停止担心。当视图控制器不存在时,标签也将不存在。这个标签的寿命比它必须的要长,但这在宏观上是微不足道的。

【讨论】:

  • 我投了赞成票。谢谢你的回答!如果你能给我提供一些读物或资料,那就太好了。谢谢!我很可能会接受这一点,但请随时添加更多细节或信息。
  • 我只是简单地列举了一些事实。这不像是有任何怀疑或辩论的余地。如果你无法理解内存管理的工作原理,你可以阅读我书中的内存管理部分:apeth.com/iOSBook/ch12.html#_memory_management 该章的免费在线版本是关于 Objective-C 的,但基本事实与 Swift 相同。事实上,我在本章的这个版本中比本书的现代版本更详细地介绍了 ARC 所做的事情,因为正如您之前提到的,ARC“正常工作”,因此不必担心这些细节。
  • 我同意。这种情况不属于内存泄漏,因为没有创建保留周期。 UILabel 保留在内存中,因为控制器持有对它的强引用,除非您为属性设置不同的标签或 nil 将其取出(这需要将其声明为可选)
  • 还有,哦,嗨,马特·纽伯格!我喜欢你的书。您的 objc 基础知识帮助我开始使用 iOS :)
  • @matt 非常感谢。我根据您所说的进行了一些研究,它帮助我学习了许多新的基础知识。
【解决方案2】:

在需要时创建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)
}
}

【讨论】:

  • 嘿,这似乎是一个聪明的解决方法。但是您是否表示我必须重新声明我在 View Controller 中引用的每个变量?这是编码中的常见做法吗?我正在寻找最佳行业实践。
  • @AkshanshThakur 是的。但是以您的方式不会导致内存问题,因为在 viewcontroller release 时所有两个强引用都会中断。
  • 好的。有没有办法可以确认这一点?任何相关来源或相关文章。我确信当视图控制器被释放时视图将被释放。但这也会从视图中释放标签吗?
  • @AkshanshThakur 试试这个演示
【解决方案3】:

我认为视图控制器的强引用变量不会导致任何内存问题。

通常在释放视图控制器之前释放视图。例如,在您的代码中,当解除分配视图时,ARC 减少指向名称标签的计数器,因此它从 2 传递到 1。然后,在解除分配视图控制器时,它再次减少计数器,从 1 到 0。一旦有0 个指向 namelabel 的引用已被删除。

【讨论】:

    【解决方案4】:

    弱引用是不保持强引用的引用 它引用的实例,因此 不会阻止 ARC 处理 引用的实例。此行为可防止引用 成为强大参考循环的一部分。你表示弱 通过在属性或变量之前放置弱关键字来引用 声明

    > 弱引用必须声明为变量,以表明它们的 值可以在运行时改变。弱引用不能声明为 常数。

    因为弱引用不会对实例保持强控制 它指的是,该实例有可能被释放,而 弱引用仍在引用它。因此,ARC 当它的实例时自动设置一个对 nil 的弱引用 指的是被释放。因为弱引用需要允许 nil as 他们的价值,他们总是有一个可选的类型。您可以检查 弱引用中存在一个值,就像任何其他的一样 可选值,你永远不会引用一个 不再存在的无效实例

    来源Apple docs

    弱引用只是一个指向对象的指针,它不能保护对象不被 ARC 释放。虽然强引用将对象的保留计数增加 1,但弱引用不会。此外,当成功解除分配时,弱引用会将指向您的对象的指针归零。这样可以确保当您访问弱引用时,它要么是有效对象,要么是 nil。

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

    【讨论】:

      【解决方案5】:

      我总是这样向我的学生解释。

      通过强引用,您可以看到一个值,并且您在它周围有一个套索。您对价值是否仍然存在有发言权。

      使用弱引用,您可以看到它,但没有套索。你对价值是否存在没有发言权。

      【讨论】:

        【解决方案6】:

        为您的情况避免一秒钟内存泄漏的发生。你可以选择马特的回答。

        为了更好地理解,在构建阶段的 MRC 标志下创建一个自定义 UILabel 类->编译源代码。

        在自定义类中,覆盖保留和释放方法。在它们上设置断点。

        在您的视图控制器中使用该自定义 UILabel 类,并打开 ARC 标志。使用亚光答案或在 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 的工作原理以及为什么在添加到 UIView 时 UILabel 的弱引用会导致崩溃。

        【讨论】:

          猜你喜欢
          • 2013-08-01
          • 2011-10-27
          • 2011-11-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-04-23
          • 1970-01-01
          • 2017-03-10
          相关资源
          最近更新 更多