Swift學習:內存管理

內存管理

  • 跟OC一樣,Swift也是採取基於引用計數的ARC內存管理方案(針對堆空間) 
class Person {
    deinit {
        print("Person.deinit")
    }
}

print(1)
var p: Person? = Person()
print(2)
p = nil
print(3)
//輸出結果爲:
//1
//2
//Person.deinit
//3

由上圖可以看出,p置爲nil時,對象銷燬,走deinit方法 

  • Swift的ARC中有3種引用:
  • 強引用(strong reference):默認情況下,引用都是強引用
  • 弱引用(weak reference):通過weak定義弱引用
  1. 必須是可選類型的var,因爲實例銷燬後,ARC會自動將弱引用設置爲nil 
  2. ARC自動給弱引用設置nil時,不會觸發屬性觀察器
class Dog { }
class Person {
    weak var dog: Dog? {
        willSet { }
        didSet { }
    }
    deinit {
        print("Person.deinit")
    }
}
  • 無主引用(unowned reference):通過unowned定義無主引用
  1. 不會產生強引用,實例銷燬後仍然存儲着實例的內存地址(類似於OC中的unsafe_unretained)
  2. 試圖在實例銷燬後訪問無主引用,會產生運行時錯誤(野指針)
  3.  Fatal error: Attempted to read an unowned reference but object 0x0 was already deallocated
class Dog { }
class Person {
    unowned var dog: Dog {
        willSet { }
        didSet { }
    }
    deinit {
        print("Person.deinit")
    }
    init() {
        self.dog = Dog()
    }
}

weak、unowned的使用限制

  • weak,unowned只能用在類實例上面

 其中的Livable協議繼承自AnyObject,所以只有類能遵守此協議,所以只有類實例能用


autoReleasepool


循環引用(Reference Cycle)

  • weak、unowned 都能解決循環引用的問題,unowned 要比weak 少一些性能消耗
  • 在生命週期中可能會變爲 nil 的使用 weak
  • 初始化賦值後再也不會變爲 nil 的使用 unowned


閉包的循環引用

  • 閉包表達式默認會對用到的外層對象產生額外的強引用(對外層對象進行了retain操作) 
  • 下面代碼會產生循環引用,導致Person對象無法釋放(看不到Person的deinit被調用)
class Person {
    var fn: (() -> ())?
    func run() { print("run")}
    deinit {
        print("deinit")
    }
}

func test() {
    let p = Person()
    //p對閉包fn有一個強引用,閉包對p也有一個強引用
    p.fn = {
        p.run()
    }
}

test() //這裏不會輸出deinit,因爲閉包產生了循環引用

通過反彙編我們可以窺探其原理,也就是引用計數的變化:

  •  在閉包表達式的捕獲列表聲明weak或unowned引用,解決循環引用問題

weak的方式:

class Person {
    var fn: (() -> ())?
    func run() { print("run")}
    deinit {
        print("deinit")
    }
}

func test() {
    let p = Person()
    //p對閉包fn有一個強引用,閉包對p也有一個強引用
    p.fn = {
        [weak p] in
        p?.run()
    }
}

test() //deinit 對象銷燬

unowned的方式:

class Person {
    var fn: (() -> ())?
    func run() { print("run")}
    deinit {
        print("deinit")
    }
}

func test() {
    let p = Person()
    //p對閉包fn有一個強引用,閉包對p也有一個強引用
    p.fn = {
        [unowned p] in
        p.run()
    }
}

test() //deinit 對象銷燬

甚至可以在捕獲列表中給P對象重命名,或設置新的參數a

  •  如果想在定義閉包屬性的同時引用self,這個閉包必須是lazy的(因爲在實例初始化完畢之後才能引用self) 
class Person {
    //lazy保證了只用調用fn函數時纔會初始化裏面的self
    lazy var fn: (() -> ()) = {
        //消除循環引用
      [weak self] in
      self?.run()
      //unowned也可以消除循環引用
      //[unowned self] in
      //self.run()
    }
    func run()  {
        print("run")
    }
    deinit {
        print("deinit")
    }
}

func test() {
    var p = Person()
    p.run()
}

test()
//輸出結果
//run
//deinit
  • 如果上邊的閉包fn內部如果用到了實例成員(屬性、方法),編譯器會強制要求明確寫出self
  • 如果lazy屬性是閉包調用的結果,那麼不用考慮循環引用的問題(因爲閉包調用後,閉包的生命週期就結束了)


@escaping

  • 非逃逸閉包、逃逸閉包,一般都是當做參數傳遞給函數
  • 非逃逸閉包:閉包調用發生在函數結束前,閉包調用在函數作用域內

  • 逃逸閉包:閉包有可能在函數結束後調用,閉包調用逃離了函數的作用域,需要通過@escaping聲明

閉包fn只是賦給了全局變量gFn,gFn可能在函數結束後調用,所以閉包fn也可能在函數結束後調用,所以要加@escaping

因爲下圖中的fn()的實現調用是在全局隊列異步函數中,所以可能在test3函數結束後調用fn,這時如果fn函數裏使用了test3函數裏的變量或方法,是會報錯的,因爲已經銷燬了,所以要加@escaping

Dispatch結合逃逸閉包實例如下:

typealias Fn = () -> ()
class Person {
    var fn: Fn
    //fn是逃逸閉包
    init(fn: @escaping Fn) {
        self.fn = fn
    }
    func run( ) {
        //DispatchQueue.global().async也是一個逃逸閉包
        //它用到了實例成員(屬性、方法),編譯器會強制要求明確寫出self,這樣會對Person的對象的生命週期產生影響,保證在DispatchQueue.global().async中調用後再銷燬
        DispatchQueue.global().async {
            self.fn()
            //如果想要保證如果self這個Person實例銷燬了就不調用fn函數,防止崩潰,就要用weak聲明self,並把實例對象p寫成可選型,代碼如下:
//            [weak p = self] in
//            p?.fn()
        }
    }
}
  • 逃逸閉包不可以捕獲inout參數

示例1: 當調用test函數,傳入一個value的地址,由於other2是逃逸閉包,有可能在test函數執行完,value已經銷燬的情況下執行,這時候傳給other2的value的地址已經銷燬,所以會報錯

示例2:  這裏的返回值是plus,是一個閉包函數,並不知道什麼時候調用,但是用到了test方法裏的參數,有可能造成和示例1裏一樣逃逸閉包的問題,所以報錯

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章