Swift 内存管理
-
跟OC一样,Swift也是采用基于引用计数的ARC内存管理方案(针对堆空间)
-
Swift的ARC中有三种引用
- 强应用(strong reference):默认情况下,引用都是强引用
- 弱引用(weak reference):通过
weak
定义弱引用
- 必须是可选类型的
var
,因为实例销毁后,ARC会自动将弱引用设置为nil
- ARC自动给弱引用设置
nil
时,不会触发属性观察器
- 无主引用(unowned reference):通过
unowned
定义无主引用
- 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似OC中的
unsafe_unretained
)- 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)
weak/unowned的使用限制
- weak、unowned只能用在类实例上面
因为一般只有类实例放堆空间,结构体、枚举一般都是不放在堆空间的
class Cat {}
protocol Actions :AnyObject {}
weak var c0: Cat?
weak var c1: AnyObject?
weak var c3: Actions?
unowned var c4: Cat?
unowned var c5: AnyObject?
unowned var c6: Actions?
上面代码编译都是没问题的。AnyObject
是可以代表任意类类型,协议Actions
也是可以的,因为它后面是Actions :AnyObject
,意思就是它的协议只能被类类型遵守。
若协议Actions
后面的冒号去掉,c3
和c6
是编译不通过的,因为此协议有可能被结构体、枚举遵守,而weak、unowned
只能用在类实例上面,所以编译器提前抛出错误,Swift是强安全语言。
Autoreleasepool
在Swift中,Autoreleasepool
是保留的,变成了一个全局的函数:
public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
使用:
class Cat {
var name: String?
init(name:String?) {
self.name = name;
}
func eat() {}
}
autoreleasepool {
let cat = Cat(name: "zhangsan")
cat.eat()
}
内存开销较大的场景(比如数千对经纬度数据在地图上绘制公交路线轨迹),可以使用自动释放池。
循环引用(Reference Cycle)
-
weak/unowned
都能解决循环引用的问题,unowned
要比weak
少一些性能消耗
weak
在实例销毁的时候又设置了一遍weak
应用为nil
,所以,性能上多了一丢丢消耗
- 在生命周期中可能会变为
nil
的对象,使用weak
- 初始化赋值后再也不会改变为
nil
的对象,使用unowned
闭包的循环引用
- 闭包表达式默认回对用到的外层对象产生额外的强引用(对外层对象进行了
retain
操作)
class Person {
var fn:(() -> ())?
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn = {
p.run()
}
}
print(1)
test()
print(2)
运行后,打印结果只有1跟2,没有deinit,说明p对象一直没有被销毁,仔细看,问题出在这里:
p.fn = {
p.run()
}
对象p里的fn方法强引用了闭包,而闭包里也强引用了对象p,两者形成循环引用,对象p也就无法释放销毁了。
我们在p.fn
处打断点,进入汇编,看看是否有强引用(retain):
我们注释掉上面代码里的
p.fn = { p.run() }
,再看汇编:注释后的引用计数
可以看出,p.fn = { p.run() }
里,闭包对p
对象进行了强引用也就是retain
操作,构成了引用计数始终为1的情况,无法释放对象。
- 在闭包表达式的捕获列表声明
weak
或unowned
引用,解决循环引用问题
func test() {
let p = Person()
p.fn = {
[weak p] in
p?.run()
}
}
func test() {
let p = Person()
p.fn = {
[unowned p] in
p.run()
}
}
func test() {
let p:Person? = Person()
p?.fn = {
[weak p] in
p?.run()
}
}
func test() {
let p:Person? = Person()
p?.fn = {
[unowned p] in
p?.run()
}
}
注意:weak弱引用必须是可选类型,所以,对象
p
后面跟上?
若是unowned
修饰p
,p
后面不用跟?
,因为p
本身就是非可选类型,unowned
默认情况下也就是非可选类型,是跟着p
走的
class Person {
var fn:((Int) -> ())?
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn = {
[weak wp = p](age) in
wp?.run()
}
}
[weak p]
是捕获列表,(age)
是参数列表,捕获列表一般是写在参数列表前面的,in
后面的就是函数体。
- 如果想在定义闭包属性的同时引用
self
,这个闭包必须是lazy
的(因为在实例初始化完毕之后才能引用self
)
class Person {
lazy var fn:(() -> ()) = {
self.run()
}
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
}
这段代码,会打印deinit
,说明对象p
释放了。
为什么呢?按说对象p
有个强引用fn
引用了闭包表达式,闭包表达式里也强应用了self
,两者形成循环应用,无法释放对象p
。
因为
fn
是lazy
修饰的,也就是说,在未调用p.fn
的时候是没有值的,也就说它后面的闭包表达不存在,自然就无法引用self
,也就不能造成循环引用。
当第一次调用p.fn()
后,才会触发fn
的初始化,创建闭包表达式赋值给fn
,这里就形成了循环引用。
解决循环引用:
lazy var fn:(() -> ()) = {
[weak weakSelf = self] in
weakSelf?.run()
}
lazy var fn:(() -> ()) = {
[unowned weakSelf = self] in
weakSelf.run()
}
一般用weak
,因为weak
比unowned
安全
- 如果
lazy
属性是闭包调用的结果,则不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)
class Person {
var age: Int = 10
lazy var getAge: Int = {
self.age
}()
deinit {
print("deinit")
}
}
func test() {
let p = Person()
print(p.getAge)
}
test()
打印结果:10 deinit
内存访问冲突(Conflicting Access to Memory)
- 内存访问冲突会在两个访问满足下面条件时发生:
- 至少一个是写入操作
- 它们访问的是同一块内存
- 它们的访问时间重叠(比如在同一个函数内)
//不存在内存访问冲突
func plus(_ num: inout Int) -> Int {
num + 1
}
var number = 1
number = plus(&number)
//存在内存访问冲突
var step = 1
func increment(_ num: inout Int) {
//此处编译没问题,但运行报错
//Simultaneous accesses to 0x100008178, but modification requires exclusive access
num += step
}
increment(&step)
increment
函数内的num += step
产生内存冲突,因为num
虽然是形参,但外面传的值还是step
的内存地址,+=
这里就造成了同一时间对同一份内存进行既读又写的操作,所以造成内存冲突。
上例代码解决内存冲突:
var step = 1
func increment(_ num: inout Int) {
num += step
}
var temp = step
increment(&temp)
step = temp
下面代码也是存在内存冲突:
func sum(_ x: inout Int, _ y: inout Int) {
x = x + y
}
struct AA {
var x: Int = 0
var y: Int = 0
mutating func add(a: inout AA) {
sum(&a.x, &a.y)
}
}
var aa = AA(x: 1, y: 2)
var bb = AA(x: 1, y: 3)
sum(&aa.x, &aa.y) //编译没问题,但内存冲突,运行报错Simultaneous accesses to 0x100008190, but modification requires exclusive access.
这句语句运行报错,是因为aa.x
aa.y
虽然是两个不同的变量,内存地址也不一样,但是它们是一个整体,都在结构体实例aa
的内存空间内,访问它们两个也就是同时访问同一个结构体内存。
所以上面代码存在内存访问冲突。
元组也一样,元组内的不同变量访问,其实也是访问同一块元组内存,只是它们内部变量的地址不同而已,外部存储变量的元组内存空间还是同一份。
- 如果下面条件可以满足,说明重叠访问结构体的属性是安全的
- 只访问实例存储属性,不是计算属性或者类属性
- 结构体是局部变量而非全局变量
- 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获
//没问题
func test() {
var aa = AA(x: 1, y: 2)
sum(&aa.x, &aa.y)
}
指针
- Swift中有专门的指针类型,这些都被定性为不安全的(Unsafe),常见有以下4种:
UnsafePointer<Pointee>
类似const Pointee *
(<Pointee>
代表泛型)UnsafeMutablePointer<Pointee>
类似Pointee *
UnsafeRawPointer
类似const void *
UnsafeMutableRawPointer
类似void *