Swift学习笔记-内存管理
避免循环引用
Swift 和 Objective-C 一样使用了自动引用计数来进行内存管理。引用计数的内存管理就无可避慢的会产生循环引用这种问题。Swift 针对这个问题也给出了 weak
和 unowned
两种声明非持有所有权的引用修饰符。二者的区别在于 weak
和 Obj-C 中的 weak
完全相同,声明一个弱引用,不会增加被引用内存的引用计数,在所指向的内存被释放时指针会自动被置为 nil
。unowned
则类似于 Obj-C 中的 unsafe_unretain
修饰符,作用和 weak
相同,但在所致内存被释放后不会被自动置为 nil
,而是保留指向原来内存空间的引用,若对象已经被释放后对其发生了方法调用则会造成崩溃。
苹果的推荐使用方法是:在能够确定在访问时对象一定没有被释放的情况下使用 unowned
,在不能够确定时使用 weak
,这么做的原因我想是因为使用 weak
时由于在内存释放时需要对指针进行置空操作,耗费了一定的性能,所以能够确定的情况下使用 unowned
比较好。
使用弱引用的情况:
- 设置 delegate 时
- 在 self 持有的闭包中引用 self 时
可以这样标注闭包中的元素,指定它们的内存管理语义:
1 | getaclosure("as") {[weak self] (name) -> Int in |
如需要标注多个元素则需要使用逗号将它们分开:
1 | getaclosure("as") {[weak self, unowned someObject] (name) -> Int in |
@autoreleasepool
Swift 同样采用 ARC 管理内存,所以 autoreleasepool 这个东西也照旧被搬了过来。自动释放池的作用是将所有需要稍后释放的对象放进去,在使用之后进行统一的释放。避免对象在尚未被使用之前就已经被释放。是一种延迟释放的方式。在 Swift 项目中不再需要像 Objective-C 那样在 main 函数手动为整个程序包一个 @autoreleasepool 了,编译器已经帮你完成了这些操作。但是有一种情况还是需要自动释放,当一个代码块生成了大量的 autorelease 对象的时候,需要使用 autoreleasepool 来包裹这部分代码。
在循环中,使用工厂方法创建对象的时候,往往创建出的都是 autorelease 对象,这是需要使用 autorelease 闭包来进行包装。防止内存突增。
1 | for _ in 1...1000 { |
以上代码会造成内存的暴涨,因为在循环过程中产生了大量的 autorelease 变量,这些变量并不会立即被释放,而是在 runloop 周期执行结束时才会被释放。正确的做法如下:
1 | for _ in 1...1000 { |
这样做就不会有问题了。
但是需要注意的是,在 Swift 1.1 之后,以上的工厂方法生成对象已经不是 Swift 推荐的做法,推荐的做法是使用可以返回 nil 的初始化方法:
1 | let date = Data(contentsOfFile: path) |
这样就不会存在自动释放的问题了,每次循环结束的时候 ARC 会自动为我们处理好内存管理的事情。
值类型和引用类型
和大多数语言一样,Swift 的数据类型分为值类型和引用类型两种,值类型在传递和复制的时候将会进行复制,引用类型则只会使用引用对象的一个指向
。
Swift 中的 struct 和 enum 定义的类型都是值类型,class 定义的类型都是引用类型。
这样一来我们就知道一个问题,Swift 的所有内建类型全部都是值类型的。甚至连集合类型都是值类型的数据。这也是 Swift 和其他主流语言的一个比较大的区别。清一色的值类型数据给 Swift 带来了较好的性能,因为相对于使用引用类型而言,使用值类型的数据可以有效的减少堆上内存的分配和回收,这无疑会有很大的性能提升。值类型的复制肯定也会产生开销,但是在 Swift 的精心设计下,应将这种复制的开销降到了最低。为什么这么说呢,看代码:
1 | func test(arr:[Int]){ |
以上代码中的 a,b,c
和方法中的 arr
事实上指向的是同一块内存,这样的代码并没有改变 a
的内部数据,Swift 对此进行了优化,在没有必要复制的情况下不会对值类型进行复制操作。这样的值类型操作完全是在栈内存上进行的,这里我个人觉得可以理解为是对栈内存的“引用”,并没有产生任何的堆内存分配和释放操作,运行效率很高。
以下代码会产生复制的动作:
1 | func test(arr:[Int]){ |
由于改变了数组的内容,为了遵守值类型语义,所以会对其进行复制操作。
对于集合类型,在复制的时候会将集合类型内部的值类型元素进行复制,集合类型内部的引用类型元素则会被复制一份引用。
1 | class testobj { |
通过以上可以看出,对于容器内容数据量小变化少,容器个数多,对内容变化不频繁的情况,使用 Swift 值类型集合会显著的提高性能,降低开销。对于容器内容数据量大,且需要频繁对容器进行内容的增减操作的情况,使用 Cocoa 提供的引用类型集合会更好,这样可以避免在改变内容的时候进行大量的复制操作。具体的使用需要根据实际情况来考虑。
C 指针的内存管理
在 Swift 中直接对指针进行操作是不提倡的,但是为了和 C 语言接口进行交互,Swift 还是提供了操作指针的方法。那就是 UnsafePointer
系列类型。这个系列目前(Swift 2.2)包括以下几种类型:
1 | UnsafePointer |
UnsafePointer
是对 C 语言指针的包装类型,在 Swift 中需要遵守统一的命名规则,对于 C 语言的基础类型在 Swift 中都有着以大写 C 开头的对应类型。如果我们 C API 接收的是一个
int*
类型的指针参数,这时对应的应该是用一个UnsafePointer<CInt>
类型作为 Swift 的参数类型。 这里 UnsafePointer 对应的是不可变的版本的const int *
常量指针UnsafeMutablePointer
是 UnsafePointer 的可变版本。
UnsafeBufferPointer
是不可变数组指针的包装类型,使用
baseAddress
取得UnsafePointer
来对数组元素使用memory
进行访问。UnsafeMutableBufferPointer
是可变数组的包装类型,使用
baseAddress
取得UnsafeMutablePointer
来对数组元素使用memory
进行访问。COpaquePointer
这是一类比较特殊的指针,用于包装 C 语言中那些在头文件中无法找到具体定义,只能拿到类型和名字的指针,即
不透明指针
。CFunctionPointer
顾名思义,函数指针。形式如:
CFunctionPointer<()->CInt>
Swift 自动引用计数无法直接管理指针类型对象的内存。这部分的内存需要我们手动来管理。对于 UnsafePointer
在 Swift 中我们无法直接创建这个类型的实例,只能创建 UnsafeMutablePointer
的实例。一般的步骤是:
1 | 通过 alloc 向系统申请一块内存; |
需要把握的原则是,内存由谁创建就要由谁销毁。除非 API 文档明确告知需要用户来负责销毁内存。alloc 和 destroy dealloc 要成对的出现。
需要说明的是,这里也可以使用 malloc 和 calloc 申请内存,Swift 提供了这两个方法均返回 UnsafeMutablePointer 类型的对象,这时需要使用 free 方法来释放内存。
对于这些直接操作内存的方法,Swift 中是十分不提倡的,因为会带来未知的错误和难以调试的 bug ,在使用这些方法之前,最好搞清楚自己在做什么,是不是真的别无他法。