強引用
Swift
使用ARC管理內存
OC
創建實例對象,默認引用計數為0
Swift
創建實例對象,默認引用計數為1
class LGTeacher{
var age: Int = 18
var name: String = "Zang"
}
var t=LGTeacher()
var t1=t
var t2=t
上述代碼,通過
LLDB
指令來查看t
的引?計數:
輸出的查看t的引?計數refCounts
為什么是0x0000000600000002
?
通過源碼進行分析,打開
HeapObhect.h
,看到一個宏
HeapObhect.h進入
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS
宏定義,這里看到refCounts
類型是InlineRefCounts
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS進入
InlineRefCounts
定義,它是RefCounts
類型的別名
InlineRefCounts進入
RefCounts
定義,它是一個模板類。后續邏輯取決于模板參數RefCountBits
,也就是上圖中傳入的InlineRefCountBits
的類型
RefCounts進入
InlineRefCountBits
定義,它是RefCountBitsT
類型的別名
InlineRefCountBits首先確認
RefCountIsInline
是什么,進入RefCountIsInline
定義,本質上是enum
,只有true
和false
。這里傳入的RefCountIsInline
就是true
RefCountIsInline再進入到
RefCountBitsT
的定義,里面的成員變量bits
,類型為BitsType
RefCountBitsT
bits
對RefCountBitsInt
的Type
屬性取別名,本質上就是uint64_t
類型
RefCountBitsInt明白了
bits
是什么,下面就來分析HeapObject
的初始化方法,重點看第二個參數refCounts
HeapObject初始化方法進入
Initialized
定義,它的本質是一個enum
,找到對應的refCounts
方法,需要分析一下傳入的RefCountBits(0, 1)
到底在做什么
Initialized進入
RefCountBits
,還是模板定義,把代碼繼續往下拉...
RefCountBits在下面找到真正的初始化方法
RefCountBitsT
,傳入strongExtraCount
和unownerCount
兩個uint32_t
類型參數,將這兩個參數根據Offsets
進行位移操作
RefCountBitsT通過源碼分析,最終我們得出這樣?個
結論
:
結論
isImmortal
(0)UnownedRefCount
(1-31):無主引用計數isDeinitingMask
(32):是否進行釋放操作StrongExtraRefCount
(33-62):強引用計數UseSlowRC
(63)
對照上述結論,使用二進制查看
refCounts
輸出的0x0000000600000002
二進制查看refCounts
1-31
位是UnownedRefCount
無主引用計數33-62
位是StrongExtraRefCount
強引用計數
通過SIL代碼,分析
t
的引用計數,當t
賦值給t1
、t2
時,觸發了copy_addr
SIL查看SIL文檔,
copy_addr
內部又觸發了strong_retain
copy_addr回到源碼,來到
strong_retain
的定義,它其實就是swift_retain
,其內部是一個宏定義CALL_IMPL
,調用的是_swift_retain_
,然后在_swift_retain_
內部又調用了object->refCounts.increment(1)
strong_retain進入
increment
方法,里面的newbits
是模板函數,其實就是64位
整形。這里我們發現incrementStrongExtraRefCount
方法點不進去,因為編譯器不知道RefCountBits
目前是什么類型
increment方法我們需要回到
HeapObject
,從InlineRefCounts
進入,找到incrementStrongExtraRefCount
方法
通過image.pngBitsType
方法將inc
類型轉換為uint64_t
,通過Offsets
偏移StrongExtraRefCountShift
,等同于1<<33
,十進制的1
左移33
位,再轉換為十六進制,得到結果0x200000000
。故此上述代碼相當于bits += 0x200000000
,左移33
位后,在33-62
位上,強引用計數+1
上述源碼分析中,多次看到
C++
的模板定義,其目是為了更好的抽象,實現代碼重用機制的一種工具。它可以實現類型參數化,即把類型定義為參數, 從而實現真正的代碼可重用性。模版可以分為兩類,一個是函數模版,另外一個是類模版。
通過
CFGetRetainCount
查看引用計數
class LGTeacher{
var age: Int = 18
var name: String = "Zang"
}
var t=LGTeacher()
print(CFGetRetainCount(t))
var t1=t
print(CFGetRetainCount(t))
var t2=t
print(CFGetRetainCount(t))
//輸出以下內容:
//2
//3
//4
上述代碼中,原本
t
的引用計數為3
,使用CFGetRetainCount
方法會導致t
的引用計數+1
弱引用
class LGTeacher{
var age: Int = 18
var name: String = "Zang"
var stu: LGStudent?
}
class LGStudent {
var age = 20
var teacher: LGTeacher?
}
func test(){
var t=LGTeacher()
weak var t1=t
print(CFGetRetainCount(t))
}
test()
//輸出以下內容:
//2
上述代碼,
t
創建實例對象引用計數默認為1
,使用CFGetRetainCount
查看引用計數+1
,打印結果為2
。顯然將t
賦值給使用weak
修飾的t1
,并沒有增加t
的強引用計數
通過
LLDB
指令來查看t
的引?計數:
將查看`t`的引?計數t
賦值給weak
修飾的t1
,查看refCounts
打印出奇怪的地址
通過
LLDB
指令來查看t1
:
使用查看t1weak
修飾的t1
變成了Optional
可選類型,因為當t
被銷毀時,t1
會被置為nil
,所以weak
修飾的變量必須為可選類型
通過斷點查看匯編代碼,發現定義
weak
變量,會調用swift_weakInit
函數
查看匯編代碼
通過源碼進行分析,找到
swift_weakInit
函數,這個函數由WeakReference
調用,相當于weak
字段在編譯器聲明過程中自定義了一個WeakReference
對象,目的在于管理弱引用。在swift_weakInit
函數內部調用了ref->nativeInit(value)
, 其中value
就是HeapObject
swift_weakInit進入
nativeInit
方法,判斷object
不為空,調用formWeakReference
nativeInit進入
formWeakReference
方法,首先通過allocateSideTable
方法創建SideTable
,如果創建成功,調用incrementWeak
formWeakReference進入
allocateSideTable
方法,先通過refCounts
拿到原有的引用計數,再通過getHeapObject
創建SideTable
,將地址傳入InlineRefCountBits
方法
allocateSideTable進入
InlineRefCountBits
方法,將參數SideTable
的地址,直接進行偏移,然后存儲到內存中,相當于將SideTable
直接存儲到uint64_t的變量中
InlineRefCountBits
之前查看
t
的refCounts
,打印出0xc0000000200d1d6e
這串奇怪的地址,去掉62位
和63位
保留字段,剩余的就是偏移后的HeapObjectSideTableEntry
實例對象的內存地址,即散列表的地址
二進制查看refCounts
回到源碼分析,進入
HeapObjectSideTableEntry
定義,里面有object
對象和refCounts
,refCounts
是一個SideTableRefCounts
類型
HeapObjectSideTableEntry進入
SideTableRefCounts
定義,它是RefCounts
類型的別名,和之前分析的InlineRefCountBits
類似,后續邏輯取決于模板參數的傳入,這里傳入的是SideTableRefCountBits
類型
SideTableRefCounts進入
SideTableRefCountBits
定義,它繼承于RefCountBitsT
SideTableRefCountBitsRefCountBitsT
存儲的是uint64_t
類型的64位
的信息,用于記錄原有引用計數。除此之外SideTableRefCountBits
自身還有一個uint32_t
的weakBits
,用于記錄弱引用計數
還原散列表地址,查看弱引用
refCounts
- 將
0xc0000000200d1d6e
地址62位
和63位
的保留字段清零,得到地址0x200D1D6E
- 將
0x200D1D6E
左移3位,還原成HeapObjectSideTableEntry
對象地址0x10068EB70
,也就是散列表地址- 通過
x/8g
讀取地址0x10068EB70
查看弱引用refCounts
循環引用
案例1:
閉包捕獲外部變量
var age = 10
let clourse = {
age += 1
}
clourse()
print(age)
//輸出以下內容:
//11
從輸出結果來看, 閉包內部對變量的修改將會改變外部原始變量的值,因為閉包會捕獲外部變量,這個與
OC
中的block
一致
案例2:
deinit
反初始化器
class LGTeacher{
var age = 18
deinit{
print("LGTeacher deinit")
}
}
func test(){
var t = LGTeacher()
}
test()
//輸出以下內容:
//LGTeacher deinit
當
test
函數里的局部變量t
被銷毀時,會執行反初始化器deinit
方法,這個與OC
中的dealloc
一致
案例3:
閉包修改實例變量的值,閉包能否對
t
造成強引用?
class LGTeacher{
var age = 18
deinit{
print("LGTeacher deinit")
}
}
func test(){
var t = LGTeacher()
let closure = {
t.age += 1
}
closure()
print("age:\(t.age)")
}
test()
//輸出以下內容:
//age:19
//LGTeacher deinit
從輸出結果來看, 閉包對
t
并沒有造成強引用
案例4
將
案例3
進行修改,在LGTeacher
類里定義閉包類型屬性completionBlock
,在test
函數內,調用t.completionBlock
閉包,內部修改t.age
屬性,這樣能否對t
造成強引用?
class LGTeacher{
var age = 18
var completionBlock: (() ->())?
deinit{
print("LGTeacher deinit")
}
}
func test(){
var t = LGTeacher()
t.completionBlock = {
t.age += 1
}
print("age:\(t.age)")
}
test()
//輸出以下內容:
//age:18
從輸出結果來看,這里產生了循環引用,沒有執行
deinit
方法,也沒有打印LGTeacher deinit
。因為實例變量t
的釋放,需要等待completionBlock
閉包的作用域釋放,但閉包又被實例對象強引用,造成循環引用,t
對象無法被釋放
案例5
案例4
中循環引用的兩種解決方法
1、使用
weak
修飾閉包傳入的參數,參數的類型是Optional
可選類型
func test(){
var t = LGTeacher()
t.completionBlock = { [weak t] in
t?.age += 1
}
print("age:\(t.age)")
}
//輸出以下內容:
//age:18
//LGTeacher deinit
2、使用
unowned
修飾閉包參數,與weak
的區別在于unowned
不允許被設置為nil,在運行期間假定它是有值的,所以使用unowned
修飾要注意野指針的情況
func test(){
var t = LGTeacher()
t.completionBlock = { [unowned t] in
t.age += 1
}
print("age:\(t.age)")
}
//輸出以下內容:
//age:18
//LGTeacher deinit
捕獲列表
[unowned t]
、[weak t]
在Swift
中叫做捕獲列表
- 捕獲列表的定義在參數列表之前
- 書寫形式:??括號括起來的表達式列表
- 如果使?捕獲列表,即使省略參數名稱、參數類型和返回類型,也必須使?
in
關鍵字[weak t]
就是獲取t
的弱引用對象,相當于OC
中的weakself
var age = 0
var height = 0.0
let closure = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
closure()
//輸出以下內容:
//0
//1.85
上述代碼中,捕獲列表的
age
是常量,并且進行了值拷貝。對于捕獲列表中的每個常量,閉包會利?周圍范圍內具有相同名稱的常量或變量,來初始化捕獲列表中定義的常量。