swift底層探索 07 -內存管理(refCount&weak&unowned)

提到內存管理在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的初始化方法.
  • InlineRefCountsrefCounts的類型.
(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步: RefCountBitsTbits類型是: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++;
  }
  1. 在聲明weak后,調用了incrementWeak自增方法;
  2. incrementWeak方法中獲取了sideTable對象;
  3. incrementWeakRefCount完成了weakBits的自增;

注:在weak引用之后,在進行strong強引用后,refCount該如何計算呢?篇幅問題就不展開了,各位可以自己試試。

三, 捕獲列表

  • [weak t] / [unowned t] 在swift中被稱為捕獲列表
  • 作用:
    1. 解決closure的循環引用;
    2. 進行外部變量的值捕獲

本次換個例子。

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是存在棧區的。
幾種基本匯編指令詳解
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容