和OC一样,Swift也是采取基于引用计数的ARC内存管理方案(针对堆空间)。
一、引用介绍
Swift的ARC中有3中引用:
- 强引用(strong reference):默认情况下,引用都是强引用
- 弱引用(weak reference):通过
weak
定义弱引用 - 无主引用(unowned reference):通过
unowned
定义无主引用
1.1. 强引用
示例代码:
class Person {
deinit {
print("Person.deinit")
}
}
func test() {
let p = Person()
}
print("1")
test()
print("2")
/*
输出:
1
Person.deinit
2
*/
test
函数中的p
是强引用,会在函数调用结束后自动释放。
1.2. 弱引用
弱引用变量必须是可选类型的var
,因为实例销毁后,ARC会自动将弱引用设置为nil
。
思考:为什么要设置为
var
?为什么必须是可选类型?
因为只有可选类型才能设置为nil
,只有var
才能改变内存。
示例代码一:
weak var p: Person? = Person()
ARC自动给弱引用设置nil
时,不会触发属性观察器。
示例代码二:
class Dog {
}
class Person {
weak var dog: Dog? {
willSet {
print("dog property willSet")
}
didSet {
print("dog property didSet")
}
}
deinit {
print("Person.deinit")
}
}
var p = Person()
print("1")
p.dog = Dog()
print("2")
/*
输出:
1
dog property willSet
dog property didSet
2
*/
上面示例代码中Dog
对象很快会被销毁并把dog
属性自动置为nil
,可以看出没有更多的属性观察器打印输出。
1.3. 无主引用
无主引用不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained
)。
试图在实例销毁后访问无主引用,会产生运行时错误(野指针)。
1.4. weak、unowned的使用限制
weak、unowned
只能用在类实例上面。
示例代码:
protocol Livable : AnyObject {
}
class Person {
}
weak var p0: Person?
weak var p1: AnyObject?
weak var p2: Livable?
unowned var p3: Person?
unowned var p4: AnyObject?
unowned var p5: Livable?
1.5. Autoreleasepool
官方定义:
public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
使用示例:
class Person {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
func run() {
print("Person run")
}
}
autoreleasepool {
let p = Person(age: 20, name: "idbeny")
p.run()
}
只需要把释放的代码放到自动释放池的尾随闭包内即可。
二、循环引用(Reference Cycle)
weak、unowned
都能解决循环引用的问题、unowned
要比weak
少一些性能消耗。
使用场景:
- 在生命周期中可能会变为
nil
的使用weak
- 初始化赋值后再也不会变为
nil
的使用unowned
示例代码一(基础):
class Person {
let name: String
init(name: String) {
self.name = name }
var apartment: Apartment?
deinit {
print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) {
self.unit = unit }
var tenant: Person?
deinit {
print("Apartment \(unit) is being deinitialized") }
}
示例代码二(循环引用):
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
即使两个变量被释放,变量指向的对象之间还是会存在互相强引用的关系,不会被销毁。
示例代码三(解决循环引用):
要想解决循环引用,只需要把其中一个变量设置为nil
。
john = nil
// 输出:John Appleseed is being deinitialized
unit4A = nil
// 输出:Apartment 4A is being deinitialized
示例代码四(使用unowned
解决循环引用):
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
由于使用的是unowned
引用,所以当断开john
变量的强引用时,就不会有强引用Customer
实例。
john = nil
// 输出:John Appleseed is being deinitialized
// 输出:Card #1234567890123456 is being deinitialized
三、闭包的循环引用
闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了retain
操作)。
下面代码会产生循环引用,导致Person
对象无法释放(看不到Person
的deinit
被调用):
class Person {
var fn: (() -> ())?
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn = {
p.run()
}
}
test()
Person
对象p
对fn
有强引用,p.fn
的闭包表达式对Person
对象有强引用,两者之间形成循环引用,所以无法释放。
引用计数最终等于1,没有释放:
3.1. 闭包表达式
在闭包表达式的捕获列表声明weak
或unowned
引用,解决循环引用问题:
// 使用weak
// p.fn = {
// [weak p] in
// p?.run()
// }
// 使用unowned
p.fn = {
[unowned p] in
p.run()
}
示例代码(带参数):
class Person {
var fn: ((Int) -> ())?
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn = {
[unowned p](Int) in
p.run()
}
}
test()
示例代码(变量别名):
p.fn = {
[weak wp = p, unowned up = p, a = 10 + 20](Int) in
wp?.run()
}
3.2. self和lazy
如果想在定义闭包属性的同时引用self
,这个闭包必须是lazy
的(因为在实例初始化完毕之后才能引用self
)。
为什么不能使用self
?
因为self
只有在实例初始化完毕后才能调用,在初始化属性的同时使用self
肯定是不行的,除非在属性前面加上lazy
(允许在实例初始化完毕之后第一次使用属性时再初始化属性)。
示例代码一:
class Person {
lazy var fn: (() -> ()) = {
self.run()
}
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
var p = Person()
}
test()
// 输出:deinit
为什么对象被释放了?
因为属性fn
没有被用到,所以属性没有对实例进行强引用。
如果加上下面的代码就会造成强引用:
func test() {
var p = Person()
p.fn()
}
怎样解决强引用呢?
在闭包表达式中使用weak
或unowned
即可。
class Person {
lazy var fn: (() -> ()) = {
[weak weakself = self] in
weakself?.run()
}
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn()
}
test()
/*
输出:
run
deinit
*/
注意:上面的闭包
fn
内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出self
(主要目的是为了提醒开发者循环引用问题)。
如果lazy
属性是闭包调用的结果,那么不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)。
示例代码:
class Person {
var age: Int = 0
lazy var getAge: Int = {
self.age
}()
deinit {
print("deinit")
}
}
func test() {
let p = Person()
print(p.getAge)
}
test()
/*
输出:
0
deinit
*/
为什么这里没有循环引用?
因为属性getAge
后面是一个立即执行的函数,函数执行完成后会立即释放self
,只把返回值给到getAge
,所以没有造成循环引用(该示例中闭包函数体内可以不写self
)。
四、逃逸闭包
非逃逸闭包、逃逸闭包,一般都是当做参数传递给函数。
非逃逸闭包: 闭包调用发生在函数结束前,闭包调用在函数作用域内。
typealias Fn = () -> ()
func test(_ fn: Fn) {
fn()
}
test {
print("1")
}
// 输出:1
fn
是非逃逸闭包。
逃逸闭包: 闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过@escaping
声明。
示例代码一:
typealias Fn = () -> ()
var gFn: Fn?
func test(_ fn: @escaping Fn) {
gFn = fn
}
test {
print("1")
}
// 无输出
fn
是逃逸闭包。
示例代码二:
func test(_ fn: @escaping Fn) {
DispatchQueue.global().async {
fn()
}
}
fn
也是逃逸闭包。
示例代码三:
typealias Fn = () -> ()
class Person {
var fn: Fn
init(fn: @escaping Fn) {
self.fn = fn
}
func run() {
DispatchQueue.global().async {
self.fn()
}
}
}
fn
是逃逸闭包。DispatchQueue.global().async
也是一个逃逸闭包。它用到了实例成员(属性、方法),编译器会强制要求明确写出self
。这里不会产生循环引用,因为仅仅是异步方法对Person
做了强引用,而Person
没有对异步方法做强引用。
如果Person
对象被释放后不需要再调用fn
函数,则需要使用弱引用:
DispatchQueue.global().async {
[weak weakself = self] in
weakself?.fn()
}
逃逸闭包
@escaping
主要是编译器让开发者知道该函数是有风险的。假设闭包用到了宿主的成员,而宿主在闭包调用前已经被销毁,这时候有可能程序会运行异常。
注意点:
逃逸闭包不可以捕获inout
参数。
五、内存访问冲突
内存访问冲突会在两个访问满足下列条件时发生:
- 至少一个是写入操作
- 它们访问的是同一块内存
- 它们的访问时间重叠(比如在同一个函数内)
示例代码一(没有冲突):
func plus(_ num: inout Int) -> Int {
num + 1
}
var number = 1
number = plus(&number)
示例代码二(存在冲突):
var step = 1
func increment(_ num: inout Int) {
num += step
}
increment(&step)
解决冲突:
var step = 1
func increment(_ num: inout Int) {
num += step
}
var copyofStep = step
increment(©ofStep)
step = copyofStep
示例代码三(函数):
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var num1 = 40
var num2 = 30
balance(&num1, &num2)
如果传入的是同一个变量就会报错:
示例代码四(结构体):
struct Player {
var name: String
var health: Int
var energy: Int
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)
如果传入的是同一个health
主体就会报错:
示例代码五(元组):
var tulpe = (health: 10, energy: 20)
balance(&tulpe.health, &tulpe.energy)
报错:Simultaneous accesses to 0x10000c338, but modification requires exclusive access.
虽然元组内部的两个变量地址不同,但元组是一块内存,所以会报错。
示例代码五(结构体):
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)
报错:Simultaneous accesses to 0x10000c338, but modification requires exclusive access.
和上面的元组一样,结构体也是一整块内存,访问时会报错。
避免冲突:
如果下面的条件可以满足,就说明重叠访问结构体的属性是安全的:
- 你只访问实例存储属性,不是计算属性或者类属性
- 结构体是局部变量而非全局变量
- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获
示例代码:
func test() {
var tulpe = (health: 10, energy: 20)
balance(&tulpe.health, &tulpe.energy)
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)
}
test()
把上面报错的示例代码放到函数体内就可以避免内存访问冲突,因为放到函数体后tulpe
和holly
就变成了局部变量。
转载:https://blog.csdn.net/yangbenben8866/article/details/116293447