Swift系列二十四 - 内存管理(一)

和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对象无法释放(看不到Persondeinit被调用):

class Person {
    var fn: (() -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
    p.fn = {
        p.run()
    }
}
test()

Person对象pfn有强引用,p.fn的闭包表达式对Person对象有强引用,两者之间形成循环引用,所以无法释放。

引用计数最终等于1,没有释放:

3.1. 闭包表达式

在闭包表达式的捕获列表声明weakunowned引用,解决循环引用问题:

// 使用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()
}

怎样解决强引用呢?

在闭包表达式中使用weakunowned即可。

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(&copyofStep)
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()

把上面报错的示例代码放到函数体内就可以避免内存访问冲突,因为放到函数体后tulpeholly就变成了局部变量。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页