Swift 管理内存

29
这个问题已经整理过了,重要的信息已经移到下面的答案中。
我对内存管理有一些问题。
我正在开发一个照片编辑应用程序,因此保持内存使用低是很重要的。 另外,我不会发布代码,因为在执行某个特定操作时没有大内存泄漏。只是在发生任何事情时会丢失几KB/MB的内存。而且检查成千上万行代码以找到几KB的问题真的很无聊 ;)
我的应用程序使用了Core Data、大量的CIFilter功能、位置信息和基本功能。
我的第一个视图只是一个表格视图,占用大约5MB的内存。 然后你拍一些照片,应用一些滤镜,将其保存到核心数据,然后返回到那个第一个视图。 是否可能真正清除除了驱动第一个视图所需的数据之外的所有内存内容(即那个非常安全和出色的5MB)? 或者,即使将所有东西都设置为nil,仍然会有一些残留吗?

奖励问题: UIImageJPEGRepresentationUIImagePNGRepresentation之间的文件大小/ CPU负载有区别吗? 我知道你可以使用JPEG方法设置压缩质量(对CPU / GPU更加困难?)。

只是尽可能地减少内存压力。


更新:

有人指出我的问题可能过于模糊。

我曾经遇到过以下问题:

  • 在某些点上,峰值内存使用率过高
  • 导航到第二个视图控制器然后返回会导致泄漏
  • 编辑图像会导致内存泄漏。
  • 对4-5张以上的图像应用滤镜会因低内存而导致崩溃,在此时没有更多的内存泄漏。(通过工具验证)

P.s 这些都是在 iPhone 4s 上测试的,而不是模拟器。

这里有一个梗,为这个网站增加些轻松的氛围。


6
只有当内存压力超过一定阈值时,ARC才会真正发挥作用。请注意,ARC是自动引用计数而不是垃圾回收器。 - Martin R
2
您尝试过在Instruments中使用快照来定位“消失的内存”吗? - Martin R
@MartinR 我追踪了大量消失的内存,发现是由于图像变量不是可选的。我已经修复了这些问题,现在正在处理小块。逐个解决是一种选择,但真正理解问题更好。ARC是一种垃圾收集类型,如果互联网没有骗我的话... - R Menke
1
你说你使用核心数据(Core Data)。当它们不再需要时,你可以尝试调查使用故障(faulting)的core data托管对象的情况。 - Matteo Piombo
1
我可能错了,但我不认为是ARC在内存压力时介入;相反,我认为是iOS本身。您的视图控制器将接收didReceiveMemoryWarning方法,并应在那里做出相应的响应。ARC管理应用程序分配的对象引用数量,并在它们的引用计数为零时将对象设置为nil并释放它们。 - BJ Miller
显示剩余3条评论
2个回答

36

这个问题已经开了很长时间,我现在觉得足够自信来回答它。


内存管理的不同层次:

硬件内存

在 Swift 中,使用 ARC 时,我们没有办法清理实际的硬件内存。我们只能让操作系统为我们完成此操作。其中一部分是使用正确的代码(可选项弱引用),另一部分是为操作系统创建时间完成其工作。

想象一下,我们有一个在所有线程上无限运行的函数。它只会执行一件事情,即加载图像、将其转换成黑白并保存。 所有图像最大为几 mb,该函数不会创建任何软件内存泄漏。 由于图像没有固定大小且可能具有不同的压缩,它们的占用空间也不相同。 此函数将始终使您的应用程序崩溃。

这种“硬件”内存泄漏是由于该函数始终占用下一个可用的内存槽引起的。

因为没有空闲时间,所以操作系统不会介入“实际清理内存”。在每次传递之间放置延迟可以完全解决此问题。


特定语言的内存管理

类型转换

某些操作对内存没有影响,而其他操作则有:

let myInt : Int = 1
Float(myInt) // this creates a new instance

尝试使用类型转换:

(myInt as Float) // this will not create a new instance.
引用类型 vs 值类型 | 类 vs 结构体 两者都有其优点和危险。 结构体由于是值类型,所以占用内存较大。这意味着当赋给另一个实例时,它们会复制它们的值,有效地将内存使用量翻倍。对此没有修复/解决方法。这就是使结构体成为结构体的原因。 没有这种行为,因为它们是引用类型。它们在赋值时不进行复制。相反,它们创建对同一对象的另一个引用ARC自动引用计数负责跟踪这些引用。每个对象都有一个引用计数器。每次将其赋值时,计数器加一。每次将一个引用设置为 nil、封闭函数结束或封闭对象 deinit 时,计数器减一。
当计数器达到0时,对象会被析构。
有一种方法可以防止实例析构,从而创建泄漏。这称为强引用循环这里有一个关于弱引用的好解释
class MyClass {

    var otherClass : MyOtherClass?

    deinit {
        print("deinit") // never gets called
    }
}

class MyOtherClass {

    var myclass : MyClass?

    deinit {
        print("deinit") // never gets called
    }
}

var classA : MyClass? = MyClass()

// sorry about the force unwrapping, don't do it like this
classA!.otherClass = MyOtherClass()
classA!.otherClass!.myclass = classA // this looks silly but in some form this happens a lot

classA = nil
// neither the MyClass nor the MyOtherClass deinitialised and we no longer have a reference we can acces. Immortalitiy reached they have.

将一个引用设置为弱引用

class MyOtherClass {

    weak var myclass : MyClass?

    deinit {
        print("deinit") // gets called
    }
}

输入输出

函数可以接收传递给它们的值。但也可以将这些值标记为“inout”。这使您能够更改传递给函数的结构体,而无需复制该结构体。这样做可以节省内存,具体取决于您传递的内容和在函数中执行的操作。

此外,使用“inout”还可以很好地实现多个返回值,而无需使用元组。

var myInt : Int = 0

// return with inout
func inoutTest(inout number: Int) {

    number += 5

}

inoutTest(&myInt)
print(myInt) // prints 5

// basic function with return creates a new instance which takes up it's own memory space
func addTest(number:Int) -> Int {

    return number + 5

}

函数式编程

状态是随时间变化的值。

函数式编程是面向对象编程的对应物。函数式编程使用不可变状态。

更多信息请参见此处

面向对象编程使用具有变化/可变状态的对象。而不是创建新值,旧值被更新。

函数式编程可能会使用更多内存。

示例:FP


可选类型

可选类型允许将事物设置为nil。这将降低类的引用计数或使结构体失效。将事物设置为nil是清理内存的最简单方法。这与ARC相辅相成。一旦将类的所有引用都设置为nil,它将失效并释放内存。

如果您不将实例创建为可选类型,则数据将保留在内存中,直到封闭函数结束或封闭类失效。您可能不知道何时会发生这种情况。可选类型可以控制什么东西存活多长时间。


API MM

许多“内存泄漏”是由您可能没有调用的“清理”函数引起的框架

一个很好的例子是UIGraphicsEndImageContext()。上下文将保留在内存中,直到调用此函数。当创建上下文的函数结束或涉及的图像设置为nil时,它不会清除。

另一个很好的例子是dismiss ViewControllers。从一个VC segue到另一个VC,然后再segue回来可能是有意义的,但是segue实际上创建了一个VC。segue返回不会销毁VC。调用dismissViewControllerAnimated()将其从内存中删除。

阅读类参考并仔细检查是否有“清理”函数。


如果确实需要Instruments找到泄漏,请查看本问题的其他答案。


这个答案在很多方面都是误导性的,有些句子是错误的。有些部分甚至与主题无关(例如那段关于函数式编程的段落也完全错误)。 - Sulthan
4
@Sulthan再次检查了所有内容。我把有关函数式编程的部分缩短到基本思想,并添加了更多信息的链接。 函数式编程倾向于使用更多的内存。当我开始编程时,我问了这个非常广泛的问题:“如何在Swift中释放内存以及如何防止使用过多的内存”。这个答案涵盖了大部分内容。如果你对它进行了负评并认为它是错误的,请提供来源。这个答案的很大一部分最初作为更新被包含在问题中。7个赞同可能是人们发现它有用和正确的结果。最近,我将其移动到了这个答案中。 - R Menke
2
@Sulthan,我只是想改进答案(如果需要的话),所以我认真地询问哪些部分是错误/误导的。我并不固执,可以接受自己犯错的事实。你能否提供更多反馈而不仅仅是投票和留下负面评论呢? - R Menke
函数式编程不一定依赖于不可变状态。有些语言是函数式的,但依赖于不可变状态,并不意味着这两个概念必然相关。"硬件内存泄漏"是一个非常奇怪的术语……操作系统(OS)与应用程序级别的内存管理没有任何关系。整个答案有点长,需要详细审查,但请相信,审查是必要的。最大的问题是实际问题不清楚,这可能是它没有被关闭为重复的唯一原因。 - Sulthan
1
@Sulthan 硬件内存泄漏可能不是一个完美的术语,但这就是发生的情况。某些操作会使内存无法使用,直到有空闲时间才会清理。这类似于内存泄漏。这是我最初提出问题时遇到的最大问题之一。函数式编程可能不依赖于不可变状态,但该概念通常与其相关联。我可以删除“函数式编程”这个术语,纯粹地描述它为可变 vs 不可变吗?在经过这么长时间后关闭一个相对受欢迎的问题是否有点过分? - R Menke

6

输入图像描述

在Xcode右上角点击您的应用程序名称。

输入图像描述

在弹出的菜单中点击“编辑方案”。

输入图像描述

确保左侧选择了“运行”,然后点击窗口顶部附近的“诊断”选项卡。

在“内存管理”标题下,勾选“启用Guard Malloc”

您还可以尝试在“日志记录”标题下勾选“分布式对象”和“malloc堆栈”

有关Guard Malloc、Guard Edges和Scribble的详细信息,请参见此处



希望这可以帮到您!


1
抱歉,我有点困惑。这样做会使Xcode禁用内存使用情况。这有什么好处?它只是隐藏了内存使用情况还是减缓了内存使用情况? - Mubashar

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