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

提到內(nèi)存管理在iOS開(kāi)發(fā)中陪毡,就不得不提ARC(自動(dòng)引用技術(shù))。本文主要討論的就是ARC在swift中是如何存儲(chǔ)下愈、計(jì)算,以及循環(huán)引用是如何解決的蕾久。
[toc]

一势似, refCount引用計(jì)數(shù)(強(qiáng)引用 + 無(wú)主引用)

先看一段簡(jiǎn)單的代碼

class classModel{
    var age : Int = 18
}
func test() {
    let c = classModel()
    var c1 = c
    var c2 = c
}
test()

通過(guò)LLDB添加斷點(diǎn)查看當(dāng)前c對(duì)象的內(nèi)存情況

圖一

  • 通過(guò)經(jīng)驗(yàn)該對(duì)象的引用計(jì)數(shù)應(yīng)該是:3
  • 可是圖一中對(duì)象內(nèi)存中refCopunt:0x0000000600000002,以及通過(guò)cfGetRetainCount(AnyObject)獲取到的引用計(jì)算看起來(lái)都是不正確的僧著。

1. cfGetRetainCount - sil解析

class classModel{
    var age : Int = 18
}
let temp = classModel()
CFGetRetainCount(temp)

編譯后的Sil文件:


圖二
  • 通過(guò)圖二sil文件很簡(jiǎn)單的看出CFGetRetainCount在調(diào)用之前對(duì)temp這個(gè)變量進(jìn)行了一次強(qiáng)引用履因,也就是引用計(jì)數(shù)加1。所以通過(guò)CFGetRetainCount獲得的引用計(jì)數(shù)需要-1才是正確的盹愚。這也印證了之前的經(jīng)驗(yàn)推論栅迄。

2. refCount - 類型的源碼

swift底層探索 01 - 類初始化&類結(jié)構(gòu)一文中有對(duì)swift類的源碼進(jìn)行過(guò)簡(jiǎn)單的解釋。

相信你一定會(huì)有疑惑:0x0000000600000002是什么皆怕?它為什么被叫做refCount霞篡,探索方法依舊是翻開(kāi)源碼!

  • 由于源碼中涉及多層嵌套+模板類+泛型端逼,所以閱讀起來(lái)還是有點(diǎn)困難的朗兵,建議自己動(dòng)手試試。swift-5.3.1源碼地址
(1) 該方法是swift對(duì)象初始化方法
  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的模板類顶滩。同時(shí)發(fā)現(xiàn)refCounts的類型也是泛型:RefCountBits余掖;
  • 通過(guò)第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 {
    //內(nèi)部變量
    BitsType bits;
    //內(nèi)部變量類型
    typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;

    ...
    //省略無(wú)關(guān)代碼
}
  • 內(nèi)部只有一個(gè)變量bits,類型為BitsType
(7) RefCountBitsInt 結(jié)構(gòu)體
template <RefCountInlinedness refcountIsInline>
struct RefCountBitsInt<refcountIsInline, 8> {
  typedef uint64_t Type;
  typedef int64_t SignedType;
};
  • 根據(jù)第6步的傳參得到RefCountBitsInt結(jié)構(gòu),以及Type == uint64_t
(8) 【總結(jié)】
  • 通過(guò)第1步,第2步,第3步,第4步: InlineRefCounts = RefCounts = RefCountBits = InlineRefCountBits = RefCountBitsT;(該關(guān)系并不嚴(yán)謹(jǐn)只是為了解釋簡(jiǎn)單)
  • 通過(guò)第6步,第7步: RefCountBitsTbits類型是:uint64_t;
  • refCounts的類型為RefCountBitsT,內(nèi)部只有一個(gè)變量bits類型為uint64_t;
  • RefCountBitsT是模板類,首地址指向唯一內(nèi)部變量bits;
  • 結(jié)論為:uint64_t : refCounts.

3. refCount - 初始化的源碼

現(xiàn)在再看0x0000000600000002知道它是一個(gè)uint64_t的值,可是內(nèi)部存儲(chǔ)了哪些值還需要查看初始化方法,觀察初始化方法做了什么礁鲁?

(1) 該方法是swift對(duì)象初始化方法
  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)) {}
    ...
    //省略無(wú)關(guān)代碼
}
  • 調(diào)用了RefCountBits的初始化方法,根據(jù)上一步中的關(guān)系對(duì)應(yīng):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對(duì)應(yīng)關(guān)系

Offsets的關(guān)系圖:
簡(jiǎn)書(shū)-月月
(5)【總結(jié)】
  • 0x0000000600000002就可以拆分為: 5部分盐欺。強(qiáng)引用的引用計(jì)數(shù)位于:33-62
0x0000000600000002 >> 33 // 引用計(jì)數(shù) = 3
  • 同樣滿足之前的論證。
補(bǔ)充1:
  • 初始化并且沒(méi)有賦值時(shí)仅醇,引用計(jì)數(shù)為0冗美,無(wú)主引用數(shù)為:1。源碼中的確也是這樣的RefCountBits(0, 1)
補(bǔ)充2:
class PersonModel{
    var age : Int = 18
}
func test() {
    let c = PersonModel()
    var c1 = c
    var c2 = c
    var c3 = c
    //增加了一個(gè)無(wú)主引用
    unowned var c4 = c
}
test()
圖三-輸出結(jié)果
  • unowned在本文的解決循環(huán)引用中會(huì)解釋析二。
  • StrongExtraRefCountShift(33-63位) : 0x0000000800000004右移33位 = 4
  • UnownedRefCountShift(1-31位) : 0x0000000800000004左移32位粉洼,右移33位。 = 2

4. 引用計(jì)數(shù)增加叶摄、減少

知道了引用計(jì)數(shù)的數(shù)據(jù)結(jié)構(gòu)初始化值属韧,現(xiàn)在就需要知道引用計(jì)數(shù)是如何增加減少,本文中以增加為例蛤吓;

通過(guò)打開(kāi)匯編宵喂,查看調(diào)用堆棧:


圖三
  • 發(fā)現(xiàn)會(huì)執(zhí)行swift_retain這個(gè)函數(shù)
swift_retain源碼
//入口函數(shù)
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))
    //引用計(jì)數(shù)在該函數(shù)進(jìn)行+1操作
    object->refCounts.increment(1);
  return object;
}
  • 后面源碼的閱讀會(huì)進(jìn)行斷點(diǎn)調(diào)試的方式。
increment
圖四

通過(guò)可執(zhí)行源碼進(jìn)行調(diào)試可執(zhí)行源碼会傲。

  • 根據(jù)斷點(diǎn)證實(shí)的確是執(zhí)行到increment函數(shù),并且新增值是1
具體計(jì)算的方法
圖五
  • 計(jì)算都是從33位開(kāi)始計(jì)算的

二锅棕, refCount 循環(huán)引用

class PersonModel{
    var teach : TeachModel?
}
class TeachModel{
    var person : PersonModel?
}

面對(duì)這樣的相互包含的兩個(gè)類拙泽,使用時(shí)一定會(huì)出現(xiàn)相互引用(循環(huán)引用)

圖六
  • deinit方法沒(méi)有調(diào)用,造成了循環(huán)引用裸燎。

1. weak關(guān)鍵字

通過(guò)OC的經(jīng)驗(yàn)奔滑,可以將其中一個(gè)值改為weak,就可以打破循環(huán)引用.

class PersonModel{
    weak var teach : TeachModel?
}
class TeachModel{
    weak var person : PersonModel?
}
圖六
  • 很顯然weak是可以的顺少。問(wèn)題是:weak做了什么呢?

2. weak 實(shí)現(xiàn)源碼

weak var weakP = PersonModel()

依舊是打開(kāi)匯編斷點(diǎn).

圖七

  • 從圖七能看出到weak是調(diào)用了swift_weak王浴。
swift_weak源碼
//weak入口函數(shù)
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

void nativeInit(HeapObject *object) {
//做一個(gè)非空判斷
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
  • 沒(méi)有找到WeakReference對(duì)象的創(chuàng)建脆炎,猜測(cè)是編譯器自動(dòng)創(chuàng)建的用來(lái)管理weak動(dòng)作.
通過(guò)formWeakReference創(chuàng)建HeapObjectSideTableEntry
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
調(diào)用allocateSideTable進(jìn)行創(chuàng)建
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
    //獲取當(dāng)前對(duì)象的原本的引用計(jì)數(shù)(uInt64_t)
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  ...
  
  // FIXME: custom side table allocator
  
  //創(chuàng)建HeapObjectSideTableEntry對(duì)象
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
    //RefCountBitsT對(duì)象進(jìn)行初始化
  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);
    //通過(guò)地址交換完成賦值
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}
  • 最終將RefCountBitsT對(duì)象(class)的地址和舊值uint_64進(jìn)行交換。
HeapObjectSideTableEntry對(duì)象
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;
}

通過(guò)源碼分析得出HeapObjectSideTableEntry對(duì)象的內(nèi)存分布

RefCountBitsT初始化

最終保存到實(shí)例對(duì)象的refcount字段的內(nèi)容(RefCountBitsT)創(chuàng)建

    //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對(duì)象的地址秒裕。

lldb驗(yàn)證

現(xiàn)在知道了refcount字段獲取規(guī)律,以及sideTable對(duì)象的內(nèi)部結(jié)構(gòu),現(xiàn)在通過(guò)lldb驗(yàn)證一下钞啸。

圖八

  • 發(fā)現(xiàn)被weak修飾之后几蜻,refcount變化成sideTable對(duì)象地址+高位標(biāo)識(shí)符
圖九
  • 將高位62,63變?yōu)?后体斩,在左移3位.
圖十
  • 0x10325D870這就是sideTable對(duì)象地址

weak_count 增加

weakcount是從第二位開(kāi)始計(jì)算的梭稚。
formWeakReference函數(shù)中出現(xiàn)了side->incrementWeak();sideTable對(duì)象創(chuàng)建完成后調(diào)用了該函數(shù).

  HeapObjectSideTableEntry* incrementWeak() {
    if (refCounts.isDeiniting())
      return nullptr;
      //沒(méi)有銷毀就調(diào)用
    refCounts.incrementWeak();
    return this;
  }
  
  void incrementWeak() {
    //獲取當(dāng)前的sideTable對(duì)象
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    RefCountBits newbits;
    do {
      newbits = oldbits;
      assert(newbits.getWeakRefCount() != 0);
      //調(diào)用核心自增函數(shù)
      newbits.incrementWeakRefCount();
      
      if (newbits.getWeakRefCount() < oldbits.getWeakRefCount())
        swift_abortWeakRetainOverflow();
        //通過(guò)值交換完成賦值
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));
  }
  
  void incrementWeakRefCount() {
  //就是一個(gè)簡(jiǎn)單++
    weakBits++;
  }
  1. 在聲明weak后,調(diào)用了incrementWeak自增方法絮吵;
  2. incrementWeak方法中獲取了sideTable對(duì)象弧烤;
  3. incrementWeakRefCount完成了weakBits的自增;

注:在weak引用之后蹬敲,在進(jìn)行strong強(qiáng)引用后暇昂,refCount該如何計(jì)算呢?篇幅問(wèn)題就不展開(kāi)了伴嗡,各位可以自己試試急波。

三, 捕獲列表

  • [weak t] / [unowned t] 在swift中被稱為捕獲列表瘪校。
  • 作用:
    1. 解決closure的循環(huán)引用澄暮;
    2. 進(jìn)行外部變量的值捕獲

本次換個(gè)例子。

class TeachModel{
    var age = 18
    var closure : (() -> Void)?
    deinit {
        print("deinit")
    }
}
func test() {
    let b = TeachModel()
    b.closure = {
        b.age += 1
    }
    print("end")
}
  • 看到這段代碼阱扬,deinit會(huì)不會(huì)執(zhí)行呢赏寇?答案是很顯然的,實(shí)例對(duì)象的閉包和實(shí)例對(duì)象相互持有价认,一定是不會(huì)釋放的嗅定。

作用1-解決循環(huán)引用

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")
}

執(zhí)行效果,都可以解決循環(huán)引用:


  • weak修飾之后對(duì)象會(huì)變?yōu)?/li>

作用2-捕獲外部變量

例如這樣的代碼:

func test() {
    var age = 18
    var height = 1.8
    var name = "Henry"
    
    height = 2.0
    //age,height被閉包進(jìn)行了捕獲
    let closure = {[age, height] in
        print(age)
        print(height)
        print(name)
    }
    
    age = 20
    height = 1.85
    name = "Wan"
    
    //猜猜會(huì)輸出什么用踩?    
    closure()
}
  • age,height被捕獲之后渠退,值雖然被外部修改但不會(huì)影響閉包內(nèi)的值忙迁。
  • 閉包捕獲的值時(shí)機(jī)為閉包聲明之前
閉包捕獲之后值發(fā)生了什么碎乃?

通過(guò)打開(kāi)匯編調(diào)試,并查看寄存器堆棧信息.


  • 猜測(cè)rdx-0x0000000100507e00姊扔,存在堆區(qū)。而閉包外的age是存在棧區(qū)的梅誓。
幾種基本匯編指令詳解
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恰梢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子梗掰,更是在濱河造成了極大的恐慌嵌言,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件及穗,死亡現(xiàn)場(chǎng)離奇詭異摧茴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)埂陆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)苛白,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人焚虱,你說(shuō)我怎么就攤上這事购裙。” “怎么了鹃栽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵缓窜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我谍咆,道長(zhǎng)禾锤,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任摹察,我火速辦了婚禮恩掷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘供嚎。我一直安慰自己黄娘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布克滴。 她就那樣靜靜地躺著逼争,像睡著了一般。 火紅的嫁衣襯著肌膚如雪劝赔。 梳的紋絲不亂的頭發(fā)上誓焦,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音着帽,去河邊找鬼杂伟。 笑死移层,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赫粥。 我是一名探鬼主播观话,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼越平!你這毒婦竟也來(lái)了频蛔?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤秦叛,失蹤者是張志新(化名)和其女友劉穎晦溪,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體书闸,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年利凑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浆劲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哀澈,死狀恐怖牌借,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情割按,我是刑警寧澤膨报,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站适荣,受9級(jí)特大地震影響现柠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弛矛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一够吩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丈氓,春花似錦周循、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至闰歪,卻和暖如春嚎研,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背库倘。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工嘉赎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留置媳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓公条,卻偏偏與公主長(zhǎng)得像拇囊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子靶橱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容