Swift学习笔记-内存管理

避免循环引用

Swift 和 Objective-C 一样使用了自动引用计数来进行内存管理。引用计数的内存管理就无可避慢的会产生循环引用这种问题。Swift 针对这个问题也给出了 weakunowned 两种声明非持有所有权的引用修饰符。二者的区别在于 weak 和 Obj-C 中的 weak 完全相同,声明一个弱引用,不会增加被引用内存的引用计数,在所指向的内存被释放时指针会自动被置为 nilunowned 则类似于 Obj-C 中的 unsafe_unretain 修饰符,作用和 weak 相同,但在所致内存被释放后不会被自动置为 nil,而是保留指向原来内存空间的引用,若对象已经被释放后对其发生了方法调用则会造成崩溃。

苹果的推荐使用方法是:在能够确定在访问时对象一定没有被释放的情况下使用 unowned,在不能够确定时使用 weak,这么做的原因我想是因为使用 weak 时由于在内存释放时需要对指针进行置空操作,耗费了一定的性能,所以能够确定的情况下使用 unowned 比较好。

使用弱引用的情况:

  1. 设置 delegate 时
  2. 在 self 持有的闭包中引用 self 时

可以这样标注闭包中的元素,指定它们的内存管理语义:

1
2
3
getaclosure("as") {[weak self] (name) -> Int in

}

如需要标注多个元素则需要使用逗号将它们分开:

1
2
3
getaclosure("as") {[weak self, unowned someObject] (name) -> Int in

}

@autoreleasepool

Swift 同样采用 ARC 管理内存,所以 autoreleasepool 这个东西也照旧被搬了过来。自动释放池的作用是将所有需要稍后释放的对象放进去,在使用之后进行统一的释放。避免对象在尚未被使用之前就已经被释放。是一种延迟释放的方式。在 Swift 项目中不再需要像 Objective-C 那样在 main 函数手动为整个程序包一个 @autoreleasepool 了,编译器已经帮你完成了这些操作。但是有一种情况还是需要自动释放,当一个代码块生成了大量的 autorelease 对象的时候,需要使用 autoreleasepool 来包裹这部分代码。

在循环中,使用工厂方法创建对象的时候,往往创建出的都是 autorelease 对象,这是需要使用 autorelease 闭包来进行包装。防止内存突增。

1
2
3
for _ in 1...1000 {
let date = NSData.dataWithContentsOfMappedFile("filename")
}

以上代码会造成内存的暴涨,因为在循环过程中产生了大量的 autorelease 变量,这些变量并不会立即被释放,而是在 runloop 周期执行结束时才会被释放。正确的做法如下:

1
2
3
4
5
6
for _ in 1...1000 {
autoreleasepool {
let date = NSData.dataWithContentsOfMappedFile("filename")

}
}

这样做就不会有问题了。

但是需要注意的是,在 Swift 1.1 之后,以上的工厂方法生成对象已经不是 Swift 推荐的做法,推荐的做法是使用可以返回 nil 的初始化方法:

1
let date = Data(contentsOfFile: path)

这样就不会存在自动释放的问题了,每次循环结束的时候 ARC 会自动为我们处理好内存管理的事情。

值类型和引用类型

和大多数语言一样,Swift 的数据类型分为值类型和引用类型两种,值类型在传递和复制的时候将会进行复制,引用类型则只会使用引用对象的一个指向

Swift 中的 struct 和 enum 定义的类型都是值类型,class 定义的类型都是引用类型。

这样一来我们就知道一个问题,Swift 的所有内建类型全部都是值类型的。甚至连集合类型都是值类型的数据。这也是 Swift 和其他主流语言的一个比较大的区别。清一色的值类型数据给 Swift 带来了较好的性能,因为相对于使用引用类型而言,使用值类型的数据可以有效的减少堆上内存的分配和回收,这无疑会有很大的性能提升。值类型的复制肯定也会产生开销,但是在 Swift 的精心设计下,应将这种复制的开销降到了最低。为什么这么说呢,看代码:

1
2
3
4
5
6
7
8
func test(arr:[Int]){
print(arr.first!)
}

var a = [1,2,3]
var b = a
let c = b
test(c)

以上代码中的 a,b,c 和方法中的 arr 事实上指向的是同一块内存,这样的代码并没有改变 a 的内部数据,Swift 对此进行了优化,在没有必要复制的情况下不会对值类型进行复制操作。这样的值类型操作完全是在栈内存上进行的,这里我个人觉得可以理解为是对栈内存的“引用”,并没有产生任何的堆内存分配和释放操作,运行效率很高。

以下代码会产生复制的动作:

1
2
3
4
5
6
7
func test(arr:[Int]){
print(arr.first!)
}

var a = [1,2,3]
var b = a
b.append(4)

由于改变了数组的内容,为了遵守值类型语义,所以会对其进行复制操作。

对于集合类型,在复制的时候会将集合类型内部的值类型元素进行复制,集合类型内部的引用类型元素则会被复制一份引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
class testobj {
var num = 0
}

var obj = testobj()
var arr = [obj]
var b = arr
b.append(obj)

obj.num = 100
print(b[0].num) //100
print(b[1].num) //100
//数组中复制的是obj的引用,对obj的操作使得b[0],b[1]同时受到了影响

通过以上可以看出,对于容器内容数据量小变化少,容器个数多,对内容变化不频繁的情况,使用 Swift 值类型集合会显著的提高性能,降低开销。对于容器内容数据量大,且需要频繁对容器进行内容的增减操作的情况,使用 Cocoa 提供的引用类型集合会更好,这样可以避免在改变内容的时候进行大量的复制操作。具体的使用需要根据实际情况来考虑。

C 指针的内存管理

在 Swift 中直接对指针进行操作是不提倡的,但是为了和 C 语言接口进行交互,Swift 还是提供了操作指针的方法。那就是 UnsafePointer 系列类型。这个系列目前(Swift 2.2)包括以下几种类型:

1
2
3
4
5
UnsafePointer
UnsafeMutablePointer
UnsafeBufferPointer
UnsafeMutableBufferPointer
COpaquePointer
  1. UnsafePointer

    是对 C 语言指针的包装类型,在 Swift 中需要遵守统一的命名规则,对于 C 语言的基础类型在 Swift 中都有着以大写 C 开头的对应类型。如果我们 C API 接收的是一个 int* 类型的指针参数,这时对应的应该是用一个 UnsafePointer<CInt> 类型作为 Swift 的参数类型。 这里 UnsafePointer 对应的是不可变的版本的const int * 常量指针

  2. UnsafeMutablePointer

    是 UnsafePointer 的可变版本。

  3. UnsafeBufferPointer

    是不可变数组指针的包装类型,使用 baseAddress 取得 UnsafePointer 来对数组元素使用 memory 进行访问。

  4. UnsafeMutableBufferPointer

    是可变数组的包装类型,使用 baseAddress 取得 UnsafeMutablePointer 来对数组元素使用 memory 进行访问。

  5. COpaquePointer

    这是一类比较特殊的指针,用于包装 C 语言中那些在头文件中无法找到具体定义,只能拿到类型和名字的指针,即不透明指针

  6. CFunctionPointer

    顾名思义,函数指针。形式如:CFunctionPointer<()->CInt>

Swift 自动引用计数无法直接管理指针类型对象的内存。这部分的内存需要我们手动来管理。对于 UnsafePointer 在 Swift 中我们无法直接创建这个类型的实例,只能创建 UnsafeMutablePointer 的实例。一般的步骤是:

1
2
3
4
5
6
通过 alloc 向系统申请一块内存;
使用 initialize 方法初始化这块内存;
然后使用这块内存;
调用 destroy 方法销毁内存;
使用 dealloc 方法销毁指针本身;
将指针置为 nil 。

需要把握的原则是,内存由谁创建就要由谁销毁。除非 API 文档明确告知需要用户来负责销毁内存。alloc 和 destroy dealloc 要成对的出现。

需要说明的是,这里也可以使用 malloc 和 calloc 申请内存,Swift 提供了这两个方法均返回 UnsafeMutablePointer 类型的对象,这时需要使用 free 方法来释放内存。

对于这些直接操作内存的方法,Swift 中是十分不提倡的,因为会带来未知的错误和难以调试的 bug ,在使用这些方法之前,最好搞清楚自己在做什么,是不是真的别无他法。