無主引用
和弱引用類似,無主引用不會牢牢保持引用的實例。但是不像弱應用,無主引用假定是永遠有值的
當我們去訪問一個無主引用的時候,總是假定有值的,所以就可能會發生程序的崩潰
如果兩個對象的生命周期并不相關,使用weak
如果非強引用對象 擁有與強引用對象相同或更長的聲明周期的話,則應使用 無主引用 unowned (也就是說 兩個對象擁有關聯 --- unowned)
結果
IFLObj1 deinit
IFLObj2 deinit
obj1 先銷毀,obj2后銷毀, obj2與obj1有關聯,IFLObj2的成員obj1 與 IFLObj1關聯在一起,不是可選值
obj1的成員 obj2 是可選值,也就是說 obj1銷毀,成員obj2也一定不存在了
因此,IFLObj2的成員 obj1可以用 無主引用 unowned
閉包循環引用
首先我們的閉包一般默認會捕獲我們的外部變量
var mVar = 10
let closure1 = {
mVar += 1
}
closure1()
print("mVar = \(mVar)")
結果
mVar = 11
從打印結果可以看出來
閉包內部對變量的修改將會改變外部原始變量的值
那同樣就會有一個問題,如果我們在class內部定義一個閉包,當前閉包訪問屬性的過程中,就會對我們當前的實例對象進行捕獲
class IFLObj1 {
var a: Int = 21
var b: String = "joewong"
var mClosure: (() -> ())?
deinit {
print("IFLObj1 deinit")
}
}
func testClosure() {
let mObj1 = IFLObj1()
mObj1.mClosure = {
mObj1.a += 2
}
}
testClosure()
IFLObj1 deinit 并未執行
控制臺查看 mObj1 引用計數
2 = (1 << 33 == 2)
mObj1 的強引用計數 為 1
注意:lldb調試的時候,不能使用 po mObj1,
因為那會增加 mObj1的引用計數,對分析造成干擾,而應該采用 api Unmanaged.passUnretained, passUnretained意思就是不對 mObj1造成引用計數+1
我們對比下不給 IFLObj1 成員 mClosure 賦值的情況
沒有對 IFLObj1 成員 mClosure 賦值的情況下
強引用計數為0, 無主引用為1
通過對比,閉包的初始化 就會對 所在類 實例對象進行捕獲,而并不需要等到閉包執行時,才捕獲
如果用po 直接查看的話,會看到 閉包初始化之后,引用計數為2,未初始化閉包前,引用計數為1
這樣就可以解釋,testClosure函數作用域內,引用計數為2,作用域結束之后,引用計數 - 1, 變為1,并未變成0,所以無法執行 deinit
而能這樣去理解嗎???
從邏輯上就可以推翻這種假設了,
那如果 直接po mObj1, po多次,就會增加多次引用,作用域結束的時候,引用計數-1,并沒有減到0,deinit并沒有執行,假設就是錯的,不能這樣簡單取巧的方式去理解
為了更嚴謹,我們就需要知道 deinit 是如何被調用執行
分析deinit調用時機
我們先通過匯編查看以下 testClosure 作用域結束前的 匯編流程,然后再找線索切入源碼查看
swift_bridgeObjectRelease 是 OC 與 swift之間的轉換部分,我們現在的代碼是swift,忽略掉這幾個影響,直接跳轉到 swift_release
進入swift_release 指令
這個時候指令跳轉進入到 swift_release
關鍵線索出現, 可是試圖通過這種方式去源碼搜索關鍵字,分析源碼邏輯,純粹從邏輯理性角度去分析,結果就是 nothiing,什么也得不出來,除了得到一些自己想當然的垃圾邏輯,基本上都是錯的,有主觀臆想在里邊
這個時候,需要借助于符號斷點 + 推斷流程 + 部分源碼,當然了,要摒棄掉po mObj1 帶來的引用計數的影響
單步符號調試+引用計數監測
為了更方便查看引用計數的變化,testClosure 作用域里,我們追加一個 mObj2的引用
deinit 敲上斷點
下載這兩個符號 重新調試 , testClosure 作用域結束前打開 下載的兩個符號
testClosure作用域內,
mObj1 無主引用計數為1
強引用計數 為 1, [ 1 << 33, 在高32位顯示為2]
arrayDestroy, 與目前我們關注的對象不符,忽略
此時,引用計數沒有變化,因為還為執行 回收
引用計數沒有變化
引用計數發生變化, 32位顯示為1,為標識位,標識當前正在進行deinit
deinit 執行
再看下 swift_deallocObjectImpl 源碼
至于 deinit 基于什么樣的源碼邏輯 調用執行,暫時可以放棄這個念頭,太繁瑣,沒有直給的邏輯,但是從前面的調試流程,可以知道
deallocClassInstance 引用計數清零,deinit標識位設置為1, 然后調用deinit
回到之前的閉包循環引用問題
通過符號進入 swift_deallocObjectImpl
引用計數依然為2
此時 引用計數 顯示的是 0
但是 deinit 標識位 并沒有設置為1, deinit未執行
分析下來,所以關鍵是這個 第32位標識位 ,為1,才會調用deinit去執行
在以上 閉包初始化前提下的分析過程中,swift_deallocObject 并未執行,反而執行的是swift_slowDealloc
源碼中有這樣的邏輯
deinit 標識位 不為1,就沒有機會執行 deinit
閉包捕獲列表
默認情況下,閉包表達式從其周圍的范圍捕獲常量和變量,并強引用這些值,我們可以使用捕獲列表來顯式控制如何在閉包中捕獲值
在參數列表之前,捕獲列表被寫為用逗號括起來的表達式列表,并用方括號括起來。如果使用捕獲列表,則即使省略參數名稱,參數類型和返回類型,也必須使用關鍵字in
var a1 = 0
var h1 = 13.1
let closure1 = { [a1] in
print("closure1, a1 = \(a1), h1 = \(h1)")
}
a1 = 10
h1 = 18.9
closure1()
結果
closure1, a1 = 0, h1 = 18.9
閉包在初始化時,就直接對 捕獲列表中的參數進行了初始化,而并不是在閉包執行時才初始化參數列表
也就是說,closure1 在初始化時, 捕獲列表中的 參數 a1就已經完成了初始化,這里的邏輯是
[let a1 = 0], 是個常量, 即使closure1執行時, 這個參數也不會再變了
而 非捕獲列表中的變量,比如 h1的捕獲 則發生在 closure1執行時,這時候就是實際h1的值了
如果改變一下
var a1 = 0
var h1 = 13.1
let closure1 = { [a1] in
print("closure1, a1 = \(a1), h1 = \(h1)")
}
a1 = 10
h1 = 18.9
closure1()
結果
closure1, a1 = 10, h1 = 18.9
閉包-延長生命周期
class IFLObj1 {
var a: Int = 21
var b: String = "joewong"
var mClosure: (() -> ())?
deinit {
print("IFLObj1 deinit")
}
}
func testClosure() {
let mObj1 = IFLObj1()
mObj1.mClosure = { [weak mObj1] in
mObj1!.a += 2
}
mObj1.mClosure!()
print("------mObj1.a = \(mObj1.a)")
}
testClosure()
結果
------mObj1.a = 23
IFLObj1 deinit
類似于OC 的方式,在block 內部 聲明強引用,延長生命周期
mObj1.mClosure = { [weak mObj1] in
if let mObj1 = mObj1 {
mObj1.a += 2
}
}
api - withExtendedLifetime 延長生命周期
withExtendedLifetime(mObj1) {
if let mObj1 = mObj1 {
mObj1.a += 2
}
}