提到內存管理在iOS開發中,就不得不提ARC(自動引用技術)。本文主要討論的就是ARC在swift中是如何存儲、計算,以及循環引用是如何解決的。
[toc]
一, refCount引用計數(強引用 + 無主引用)
先看一段簡單的代碼
class classModel{
var age : Int = 18
}
func test() {
let c = classModel()
var c1 = c
var c2 = c
}
test()
通過LLDB
添加斷點查看當前c
對象的內存情況
- 通過經驗該對象的引用計數應該是:3
- 可是
圖一
中對象內存中refCopunt:0x0000000600000002
,以及通過cfGetRetainCount(AnyObject)獲取到的引用計算看起來都是不正確的。
1. cfGetRetainCount - sil解析
class classModel{
var age : Int = 18
}
let temp = classModel()
CFGetRetainCount(temp)
編譯后的Sil文件:
- 通過
圖二
的sil
文件很簡單的看出CFGetRetainCount
在調用之前對temp
這個變量進行了一次強引用
,也就是引用計數加1
。所以通過CFGetRetainCount
獲得的引用計數需要-1
才是正確
的。這也印證了之前的經驗推論。
2. refCount - 類型的源碼
swift底層探索 01 - 類初始化&類結構一文中有對swift類的源碼進行過簡單的解釋。
相信你一定會有疑惑:0x0000000600000002
是什么?它為什么被叫做refCount
,探索方法依舊是翻開源碼!
- 由于源碼中涉及
多層嵌套+模板類+泛型
,所以閱讀起來還是有點困難的,建議自己動手試試。swift-5.3.1源碼地址
(1) 該方法是swift對象
的初始化方法
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
- 其中
refCounts(InlineRefCounts::Initialized)
就是refCounts
的初始化方法. -
InlineRefCounts
是refCounts
的類型.
(2) InlineRefCounts類型
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
- InlineRefCounts是重命名
InlineRefCounts = RefCounts
(3) RefCounts類
template <typename RefCountBits>
class RefCounts {
std::atomic<RefCountBits> refCounts;
...
//省略方法
}
-
RefCounts
是依賴泛型:RefCountBits
的模板類。同時發現refCounts
的類型也是泛型:RefCountBits
; - 通過第2步,第3步:
RefCounts = RefCountBits = InlineRefCountBits
(4) InlineRefCountBits類型
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
- InlineRefCountBits也是重命名
-
InlineRefCountBits = RefCountBitsT
;
(5) RefCountIsInline枚舉
enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };
- 傳入枚舉值:
RefCountIsInline = true
(6) RefCountBitsT 核心類
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
//內部變量
BitsType bits;
//內部變量類型
typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
BitsType;
...
//省略無關代碼
}
- 內部只有一個變量
bits
,類型為BitsType
(7) RefCountBitsInt 結構體
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
typedef uint64_t Type;
typedef int64_t SignedType;
};
- 根據第6步的傳參得到
RefCountBitsInt
結構,以及Type == uint64_t
(8) 【總結】
- 通過第1步,第2步,第3步,第4步:
InlineRefCounts = RefCounts = RefCountBits = InlineRefCountBits = RefCountBitsT
;(該關系并不嚴謹只是為了解釋簡單) - 通過第6步,第7步:
RefCountBitsT
中bits
類型是:uint64_t
; -
refCounts
的類型為RefCountBitsT
,內部只有一個變量bits
類型為uint64_t
; - 而
RefCountBitsT
是模板類,首地址指向唯一內部變量bits
; -
結論為:
uint64_t : refCounts
.
3. refCount - 初始化的源碼
現在再看0x0000000600000002
知道它是一個uint64_t
的值,可是內部存儲了哪些值還需要查看初始化方法
,觀察初始化方法做了什么?
(1) 該方法是swift對象
的初始化方法
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized)
{ }
-
Initialized
初始化
(2) RefCounts
的初始化方法
template <typename RefCountBits>
class RefCounts {
std::atomic<RefCountBits> refCounts;
enum Initialized_t { Initialized };
constexpr RefCounts(Initialized_t)
: refCounts(RefCountBits(0, 1)) {}
...
//省略無關代碼
}
- 調用了
RefCountBits
的初始化方法,根據上一步中的關系對應:RefCountBits = InlineRefCountBits = RefCountBitsT
(3) RefCountBitsT
的初始化方法
constexpr
RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
: bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
(BitsType(1) << Offsets::PureSwiftDeallocShift) |
(BitsType(unownedCount) << Offsets::UnownedRefCountShift))
{ }
(4)Offsets對應關系
Offsets的關系圖:簡書-月月
(5)【總結】
-
0x0000000600000002
就可以拆分為: 5部分。強引用的引用計數位于:33-62
位
0x0000000600000002 >> 33 // 引用計數 = 3
- 同樣滿足之前的論證。
補充1:
- 初始化并且沒有賦值時,
引用計數為0,無主引用數為:1
。源碼中的確也是這樣的RefCountBits(0, 1)
補充2:
class PersonModel{
var age : Int = 18
}
func test() {
let c = PersonModel()
var c1 = c
var c2 = c
var c3 = c
//增加了一個無主引用
unowned var c4 = c
}
test()
-
unowned
在本文的解決循環引用中會解釋。 - StrongExtraRefCountShift(33-63位) :
0x0000000800000004
右移33位 =4
- UnownedRefCountShift(1-31位) :
0x0000000800000004
左移32位,右移33位。 =2
4. 引用計數增加、減少
知道了引用計數的數據結構
和初始化值
,現在就需要知道引用計數是如何增加
和減少
,本文中以增加為例;
通過打開匯編,查看調用堆棧:
- 發現會執行
swift_retain
這個函數
swift_retain源碼
//入口函數
HeapObject *swift::swift_retain(HeapObject *object) {
CALL_IMPL(swift_retain, (object));
}
static HeapObject *_swift_retain_(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
if (isValidPointerForNativeRetain(object))
//引用計數在該函數進行+1操作
object->refCounts.increment(1);
return object;
}
- 后面源碼的閱讀會進行
斷點調試
的方式。
increment
通過可執行源碼進行調試可執行源碼。
- 根據斷點證實的確是執行到
increment
函數,并且新增值是1
具體計算的方法
- 計算都是從
33位
開始計算的
二, refCount 循環引用
class PersonModel{
var teach : TeachModel?
}
class TeachModel{
var person : PersonModel?
}
面對這樣的相互包含的兩個類,使用時一定會出現相互引用(循環引用)
-
deinit
方法沒有調用,造成了循環引用。
1. weak關鍵字
通過OC的經驗,可以將其中一個值改為weak,就可以打破循環引用.
class PersonModel{
weak var teach : TeachModel?
}
class TeachModel{
weak var person : PersonModel?
}
- 很顯然
weak
是可以的。問題是:weak做了什么呢?
2. weak 實現源碼
weak var weakP = PersonModel()
依舊是打開匯編斷點
.
- 從圖七能看出到weak是調用了
swift_weak
。
swift_weak源碼
//weak入口函數
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
void nativeInit(HeapObject *object) {
//做一個非空判斷
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
- 沒有找到
WeakReference
對象的創建,猜測是編譯器自動創建的用來管理weak動作
.
通過formWeakReference創建HeapObjectSideTableEntry
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
auto side = allocateSideTable(true);
if (side)
return side->incrementWeak();
else
return nullptr;
}
調用allocateSideTable進行創建
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
//獲取當前對象的原本的引用計數(uInt64_t)
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
...
// FIXME: custom side table allocator
//創建HeapObjectSideTableEntry對象
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
//RefCountBitsT對象進行初始化
auto newbits = InlineRefCountBits(side);
do {
if (oldbits.hasSideTable()) {
auto result = oldbits.getSideTable();
delete side;
return result;
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
return nullptr;
}
side->initRefCounts(oldbits);
//通過地址交換完成賦值
} while (! refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_release,
std::memory_order_relaxed));
return side;
}
- 最終將
RefCountBitsT
對象(class)的地址和舊值uint_64
進行交換。
HeapObjectSideTableEntry對象
class HeapObjectSideTableEntry {
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
...
}
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
//weak_count
uint32_t weakBits;
}
class RefCountBitsT {
//Uint64_t就是strong_count | unowned_count
BitsType bits;
}
通過源碼分析得出HeapObjectSideTableEntry
對象的內存分布
RefCountBitsT初始化
最終保存到實例對象的refcount字段的內容(RefCountBitsT)創建
//Offsets::SideTableUnusedLowBits = 3
//SideTableMarkShift 高位 62位
//UseSlowRCShift 高位 63位
RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
| (BitsType(1) << Offsets::UseSlowRCShift)
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}
-
62位,63位改為0 -> 整體左移3位
: 就可以得到sideTable對象
的地址。
lldb驗證
現在知道了refcount字段獲取規律,以及sideTable對象
的內部結構,現在通過lldb驗證一下。
- 發現被weak修飾之后,refcount變化成
sideTable對象地址
+高位標識符
- 將高位62,63變為0后,在左移3位.
-
0x10325D870
這就是sideTable對象地址
weak_count 增加
weakcount是從第二位開始計算的。
在formWeakReference
函數中出現了side->incrementWeak();
在sideTable對象
創建完成后調用了該函數.
HeapObjectSideTableEntry* incrementWeak() {
if (refCounts.isDeiniting())
return nullptr;
//沒有銷毀就調用
refCounts.incrementWeak();
return this;
}
void incrementWeak() {
//獲取當前的sideTable對象
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
RefCountBits newbits;
do {
newbits = oldbits;
assert(newbits.getWeakRefCount() != 0);
//調用核心自增函數
newbits.incrementWeakRefCount();
if (newbits.getWeakRefCount() < oldbits.getWeakRefCount())
swift_abortWeakRetainOverflow();
//通過值交換完成賦值
} while (!refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_relaxed));
}
void incrementWeakRefCount() {
//就是一個簡單++
weakBits++;
}
- 在聲明weak后,調用了
incrementWeak
自增方法; - 在
incrementWeak
方法中獲取了sideTable
對象; - 在
incrementWeakRefCount
完成了weakBits
的自增;
注:在weak引用之后,在進行strong強引用后,refCount該如何計算呢?篇幅問題就不展開了,各位可以自己試試。
三, 捕獲列表
-
[weak t]
/[unowned t]
在swift中被稱為捕獲列表
。 - 作用:
- 解決closure的循環引用;
- 進行外部變量的值捕獲
本次換個例子。
class TeachModel{
var age = 18
var closure : (() -> Void)?
deinit {
print("deinit")
}
}
func test() {
let b = TeachModel()
b.closure = {
b.age += 1
}
print("end")
}
- 看到這段代碼,deinit會不會執行呢?答案是很顯然的,
實例對象的閉包和實例對象相互持有
,一定是不會釋放
的。
作用1-解決循環引用
func test() {
let b = TeachModel()
b.closure = {[weak b] in
b?.age += 1
}
print("end")
}
func test() {
let b = TeachModel()
b.closure = {[unowned b] in
b?.age += 1
}
print("end")
}
執行效果,都可以解決循環引用:
- weak修飾之后對象會變為
作用2-捕獲外部變量
例如這樣的代碼:
func test() {
var age = 18
var height = 1.8
var name = "Henry"
height = 2.0
//age,height被閉包進行了捕獲
let closure = {[age, height] in
print(age)
print(height)
print(name)
}
age = 20
height = 1.85
name = "Wan"
//猜猜會輸出什么?
closure()
}
-
age
,height
被捕獲之后,值雖然被外部修改但不會影響閉包內的值
。 - 閉包捕獲的值時機為
閉包聲明之前
。
閉包捕獲之后值發生了什么?
通過打開匯編調試,并查看寄存器堆棧信息.
- 猜測
rdx-0x0000000100507e00
,存在堆區。而閉包外的age
是存在棧區的。