內存管理
- 跟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定義弱引用
- 必須是可選類型的var,因爲實例銷燬後,ARC會自動將弱引用設置爲nil
- ARC自動給弱引用設置nil時,不會觸發屬性觀察器
class Dog { }
class Person {
weak var dog: Dog? {
willSet { }
didSet { }
}
deinit {
print("Person.deinit")
}
}
- 無主引用(unowned reference):通過unowned定義無主引用
- 不會產生強引用,實例銷燬後仍然存儲着實例的內存地址(類似於OC中的unsafe_unretained)
- 試圖在實例銷燬後訪問無主引用,會產生運行時錯誤(野指針)
- 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裏一樣逃逸閉包的問題,所以報錯