swift-内存管理

Posted by sunzhongliang on March 11, 2020

内存管理

SwiftOC一样,也是基于引用计数的ARC内存管理方案(堆空间), Swift中的ARC有3种引用:

  • 强引用(strong reference)
    • 默认情况下,引用都是强引用
  • 弱引用(weak reference)
    • 通过weak定义弱引用; 必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用置为nil(ARC自动给弱引用设置nil时不会触发属性观察器)
  • 无主引用(unowned reference)
    • 通过unowned定义无主引用, 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC里的unsafe_unretained), 如果试图在实例销毁后访问无主引用,会产生运行时错误(野指针)

weak、unowned的使用限制

weak、unowned只能用在类实例上面

// 协议写上AnyObject的,只能被类遵守
protocol Livable : AnyObject {}

class Person {
    
}

weak var p0: Person?
weak var p1: AnyObject?  
weak var p2: Livable? // 协议写上AnyObject的,只能被类遵守,因此该协议也可以使用weak修饰

unowned var p10: Person?
unowned var p11: AnyObject?
unowned var p12: Livable?

自动释放池(AutoreleasePool)

自动释放池swift中的定义为:

public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result

使用方式:

autoreleasepool {
    let person = Person(age:20, name:"jack")
    person.run()
}

循环引用(Reference Cycle)

weakunowned都能解决循环引用的问题,但unowned要比weak少一些性能消耗(weak在实例销毁后要设置实例为nil)
在生命周期中可能会变为nil的建议使用weak
初始化赋值之后再也不会变为nil的建议使用unowned

闭包的循环引用
闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行retain操作)

class Person {
    // 定义fn属性
    var fn: (() -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("析构函数执行了")
    }
}

func test() {
    let p = Person()
    p.fn = {
        // 此闭包对外面的p产生了强引用
        p.run()
    }
}
// 执行test方法后,出作用域后并没有执行析构函数
test()

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

class Person {
    // 定义fn属性
    var fn: ((Int) -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("析构函数执行了")
    }
}

func test() {
    let p = Person()
    p.fn = {
        // 在闭包表达式的捕获列表声明weak/unowned引用,解决循环引用问题
        // [weak p]代表捕获列表
        // (age)代表参数列表
        // in后面的是函数体
        [weak p](age) in
        p?.run() // 弱引用必须是可选类型,因此调用时加问号

        // 使用unowned的方式为
        // [unowned p] in
        // p.run()
    }
}
// 执行test方法后,出作用域后执行了析构函数
test()

捕获列表还可以定义新的名称

p.fn = {
        [weak wp = p, unowned up = p, a = 10 + 20](age) in
        wp?.run()
    }

如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的(因为在实例初始化完毕之后才能引用self)

class Person {
    // 定义闭包表达式fn
    lazy var fn: (() -> ()) = {
        // 由于self在实例初始化完成之后才能使用,所以直接调用编译会报错
        // 解决的方法是使用lazy修饰闭包
        // 如果访问到了self变量,在没有调用fn闭包的时候是没问题的,一旦调用了fn闭包,同样会产生循环引用
        // 解决的方式就是使用weak或者unowned修饰
        [weak p = self] in
        p?.run()
    }
    func run() {
        
    }
    deinit {
        print("析构函数执行了")
    }
}

var p = Person()
p.fn()

@escaping

闭包分为逃逸闭包非逃逸闭包,一般都是当做参数传递给函数

  • 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内

      // 非逃逸闭包
      func test(_ fn: () -> ()) {
            
      }
    
      test {
          print(1)
      }
    
  • 逃逸闭包:闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过@escaping声明

      import Dispatch
      typealias Fn = () -> ()
    
      // fn是逃逸闭包
      var gFn: Fn?
      func test2(_ fn: @escaping Fn) {gFn = fn}
    
      // fn是逃逸闭包
      func test3(_ fn: @escaping Fn) {
          DispatchQueue.global().async {
              fn()
          }
      }
    

逃逸闭包不能捕获inout参数

typealias Fn = () -> ()
func other1(_ fn: Fn) {
    fn()
}
func other2(_ fn: @escaping Fn) {
    fn()
}

func test(value: inout Int) -> Fn {
    other1 {
        value += 1
    }
    
    // error: 逃逸闭包不能捕获inout参数
    func plus() {
        value += 1
    }
    
    // error: 逃逸闭包不能捕获inout参数
    return plus
}

内存访问冲突(Conflicting Access to Memory)

内存访问冲突会在两个访问满足下列条件时发生:

  • 至少一个是写入操作
  • 它们访问的是同一块内存
  • 它们访问时间重叠(比如在同一个函数内)
// 不存在内存访问冲突
func plus(_ num: inout Int) -> Int {
    num + 1
}
var number = 1
number = plus(&number)

// 存在内存访问冲突
// 运行报错:Thread 1: Simultaneous accesses to 0x10001df40, but modification requires exclusive access
var step = 1
func increment(_ num: inout Int) {
    num += step
}
increment(&step)

// ------解决内存访问冲突--------
var copyOfStep = step
increment(&copyOfStep)
step = copyOfStep

如果下面的条件可以满足,就说明重叠访问结构体的属性是线程安全的

  • 只访问实例存储属性,不是计算属性或者类属性
  • 结构体是局部变量而非全局变量
  • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获

指针

swift钟也有专门的指针类型,这些都被定性为Unsafe(不安全的), 常见的有以下四种:

  • UnsafePointer (类似于`const Pointee *`)
  • UnsafeMutablePointer (类似于`Pointee *`)
  • UnsafeRawPointer (类似于const void *)
  • UnsafeMutableRawPointer (类似于void *)

指针的应用

可以直接通过指针修改变量值

var age = 10

func test1(_ ptr: UnsafeMutablePointer<Int>) {
    ptr.pointee = 20
    print("test1:", ptr.pointee)
}

func test2(_ ptr: UnsafePointer<Int>) {
    print("test2:", ptr.pointee)
}

func test3(_ ptr: UnsafeRawPointer) {
    print("test3:",ptr.load(as: Int.self))
}

func test4(_ ptr: UnsafeMutableRawPointer) {
    ptr.storeBytes(of: 30, as: Int.self)
}

test1(&age) // 输出test1: 20
test2(&age) // 输出test2: 20
print(age) // 输出20

test3(&age) // 输出test3: 20
test4(&age)
print(age) // 输出30

在遍历数组元素时,可使用指针设置条件:

var arr = NSArray(objects: 11,22,33,44)
arr.enumerateObjects { (element, idx, stop) in
    if idx == 2 {
        stop.pointee = true
    }
}

获得指向某个变量的指针

var age = 11
var ptr1 = withUnsafeMutablePointer(to: &age) { $0 }
var ptr2 = withUnsafePointer(to: &age) { $0 }
ptr1.pointee = 22
print(ptr2.pointee) // 22
print(age) // 22

var ptr3 = withUnsafeMutablePointer(to: &age) { UnsafeMutableRawPointer($0) }
var ptr4 = withUnsafePointer(to: &age) { UnsafeRawPointer($0) }
ptr3.storeBytes(of: 33, as: Int.self)
print(ptr4.load(as: Int.self)) // 33
print(age) // 33

获得指向堆空间的指针

class Person {}
var person = Person()
var ptr = withUnsafePointer(to: &person) { UnsafeRawPointer($0) }
var heapPtr = UnsafeRawPointer(bitPattern: ptr.load(as: UInt.self))
print(heapPtr!) // 0x00000001005342d0

创建指针

// 创建
var ptr = malloc(16)
// 存
ptr?.storeBytes(of: 11, as: Int.self)
ptr?.storeBytes(of: 22, toByteOffset: 8, as: Int.self)
// 取
print((ptr?.load(as: Int.self))!) // 11
print((ptr?.load(fromByteOffset: 8, as: Int.self))!) // 22
// 销毁
free(ptr)

或者这样

var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
ptr.storeBytes(of: 11, as: Int.self)
ptr.advanced(by: 8).storeBytes(of: 22, as: Int.self)
print(ptr.load(as: Int.self)) // 11
print(ptr.advanced(by: 8).load(as: Int.self)) // 22
ptr.deallocate()

复杂类型指针创建

class Person {
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    deinit {
        print(name,"deinit")
    }
}

// 创建一个容量为3的指针
var ptr = UnsafeMutablePointer<Person>.allocate(capacity: 3)
ptr.initialize(to: Person(age: 20, name: "jack"))
// ptr+1代表是指针空间的下一个(因为创建了有3个容量)
(ptr + 1).initialize(to: Person(age: 21, name: "rose"))
(ptr + 2).initialize(to: Person(age: 22, name: "kate"))

// 也可以通过下标去访问
ptr[2].name = "kate1"


// 销毁
ptr.deinitialize(count: 3)

指针之间的切换

可通过关键字assumingMemoryBound将指针类型转换

// UnsafeMutableRawPointer类型指针
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)

// 指针转换
ptr.assumingMemoryBound(to: Int.self).pointee = 11

(ptr + 8).assumingMemoryBound(to: Double.self).pointee = 22.0

print(unsafeBitCast(ptr, to: UnsafePointer<Int>.self).pointee) // 11
print(unsafeBitCast(ptr + 8, to: UnsafePointer<Double>.self).pointee) // 22.0

ptr.deallocate()

unsafeBitCast是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据
类似于c++中的reinterpret_cast

本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.