swift指針&內存管理-閉包的循環引用

swift指針&內存管理-引用

無主引用

和弱引用類似,無主引用不會牢牢保持引用的實例。但是不像弱應用,無主引用假定是永遠有值的

當我們去訪問一個無主引用的時候,總是假定有值的,所以就可能會發生程序的崩潰

如果兩個對象的生命周期并不相關,使用weak

如果非強引用對象 擁有與強引用對象相同或更長的聲明周期的話,則應使用 無主引用 unowned (也就是說 兩個對象擁有關聯 --- unowned)

image.png

結果

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 并未執行

image.png

控制臺查看 mObj1 引用計數

2 = (1 << 33 == 2)

mObj1 的強引用計數 為 1

注意:lldb調試的時候,不能使用 po mObj1,

因為那會增加 mObj1的引用計數,對分析造成干擾,而應該采用 api Unmanaged.passUnretained, passUnretained意思就是不對 mObj1造成引用計數+1

我們對比下不給 IFLObj1 成員 mClosure 賦值的情況

image.png

沒有對 IFLObj1 成員 mClosure 賦值的情況下

強引用計數為0, 無主引用為1

通過對比,閉包的初始化 就會對 所在類 實例對象進行捕獲,而并不需要等到閉包執行時,才捕獲

如果用po 直接查看的話,會看到 閉包初始化之后,引用計數為2,未初始化閉包前,引用計數為1

這樣就可以解釋,testClosure函數作用域內,引用計數為2,作用域結束之后,引用計數 - 1, 變為1,并未變成0,所以無法執行 deinit

而能這樣去理解嗎???

從邏輯上就可以推翻這種假設了,

那如果 直接po mObj1, po多次,就會增加多次引用,作用域結束的時候,引用計數-1,并沒有減到0,deinit并沒有執行,假設就是錯的,不能這樣簡單取巧的方式去理解

為了更嚴謹,我們就需要知道 deinit 是如何被調用執行

分析deinit調用時機

我們先通過匯編查看以下 testClosure 作用域結束前的 匯編流程,然后再找線索切入源碼查看

image.png
image.png

swift_bridgeObjectRelease 是 OC 與 swift之間的轉換部分,我們現在的代碼是swift,忽略掉這幾個影響,直接跳轉到 swift_release

進入swift_release 指令

image.png

image.png

這個時候指令跳轉進入到 swift_release

image.png
image.png

關鍵線索出現, 可是試圖通過這種方式去源碼搜索關鍵字,分析源碼邏輯,純粹從邏輯理性角度去分析,結果就是 nothiing,什么也得不出來,除了得到一些自己想當然的垃圾邏輯,基本上都是錯的,有主觀臆想在里邊

這個時候,需要借助于符號斷點 + 推斷流程 + 部分源碼,當然了,要摒棄掉po mObj1 帶來的引用計數的影響

單步符號調試+引用計數監測

為了更方便查看引用計數的變化,testClosure 作用域里,我們追加一個 mObj2的引用

deinit 敲上斷點

image.png
image.png

下載這兩個符號 重新調試 , testClosure 作用域結束前打開 下載的兩個符號

image.png

testClosure作用域內,

mObj1 無主引用計數為1

強引用計數 為 1, [ 1 << 33, 在高32位顯示為2]

image.png

arrayDestroy, 與目前我們關注的對象不符,忽略

image.png

此時,引用計數沒有變化,因為還為執行 回收

image.png

image.png

引用計數沒有變化

image.png

引用計數發生變化, 32位顯示為1,為標識位,標識當前正在進行deinit

deinit 執行

再看下 swift_deallocObjectImpl 源碼

image.png
image.png

至于 deinit 基于什么樣的源碼邏輯 調用執行,暫時可以放棄這個念頭,太繁瑣,沒有直給的邏輯,但是從前面的調試流程,可以知道

deallocClassInstance 引用計數清零,deinit標識位設置為1, 然后調用deinit

回到之前的閉包循環引用問題

通過符號進入 swift_deallocObjectImpl

image.png

引用計數依然為2

image.png

此時 引用計數 顯示的是 0

image.png

但是 deinit 標識位 并沒有設置為1, deinit未執行

分析下來,所以關鍵是這個 第32位標識位 ,為1,才會調用deinit去執行

在以上 閉包初始化前提下的分析過程中,swift_deallocObject 并未執行,反而執行的是swift_slowDealloc

源碼中有這樣的邏輯

image.png
image.png

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
    }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容