Swift中的unowned和weak

基礎(chǔ)知識

SwiftObjective-C都是利用古老且有效的ARC(Automatic Reference Counting)來管理內(nèi)存惦界,當(dāng)實例的引用計數(shù)為0時,實例將會被析構(gòu),實例占有的內(nèi)存和資源都將重新變得可用。

但收擦,當(dāng)兩個實例發(fā)生循環(huán)引用時,那么他們的引用計數(shù)會一直大于0囊卜,那么他們將不會被析構(gòu)。

為了解決這個問題,SwiftObjective-C引入了弱引用栅组,弱引用不會被ARC計算雀瓢。也就是說當(dāng)一個弱引用指向一個引用類型實例時,引用計數(shù)不會增加玉掸。

Swift中的弱引用

這里以閉包為例刃麸,在OC中,標(biāo)準(zhǔn)的做法是司浪,定義一個弱引用指向閉包外部的實例泊业,然后在閉包內(nèi)部定義強引用指向這個實例,在閉包執(zhí)行期間使用它.

類似于下面代碼:

__weak typeof(self) weakSelf = self;
    void (^myBlock)(NSString *) = ^(NSString * name) {
        __strong typeof(self) strongSelf = self;
        NSString *name = self.name;
    };

為了更方便的處理循環(huán)引用啊易,Swift引入了一個新的概念吁伺,用于簡化和更加明顯的表達在閉包內(nèi)部,外部變量的捕獲:捕獲列表(capture list)认罩。使用捕獲列表,可以在函數(shù)(閉包)的頭部定義和指定那些需要用在內(nèi)部的外部變量续捂,并且指定引用類型(這里是指 unownedweak)垦垂。

例子:

不使用捕獲列表時,閉包將會創(chuàng)建一個外部變量的強引用牙瓢。

var i1 = 1, i2 = 1

var fStrong = {
    i1 += 1
    i2 += 2
}

fStrong()
print(i1,i2) //Prints 2 and 3

使用捕獲列表劫拗,閉包內(nèi)部會創(chuàng)建一個新的可用常量。如果沒有指定常量修飾符矾克,閉包將會簡單地拷貝原始值到新的變量中页慷,對于值類型和引用類型都是一樣的。

var fCopy = { [i1] in
    print(i1,i2)
}

fStrong()
print(i1,i2) //打印結(jié)果是 2 和 3  

fCopy()  //打印結(jié)果是 1 和 3

在上面的例子中胁附,在調(diào)用fStrong 之前定義函數(shù) fCopy ,在該函數(shù)定義的時候酒繁,私有常量已經(jīng)被創(chuàng)建了。正如你所看到的控妻,當(dāng)調(diào)用第二個函數(shù)時候州袒,仍然打印 i1 的原始值。

對于外部引用類型的變量弓候,在捕獲列表中指定 weakunowned郎哭,這個常量將會被初始化為一個弱引用,指向原始值菇存,這種指定的捕獲方式就是用來處理循環(huán)引用的方式夸研。

class aClass{
    var value = 1
}

var c1 = aClass()
var c2 = aClass()

var fSpec = { [unowned c1, weak c2] in
    c1.value += 1
    if let c2 = c2 {
        c2.value += 1
    }
}

fSpec()
print(c1.value,c2.value) //Prints 2 and 2

兩個 aClass 捕獲實例的不同的定義方式,決定了它們在閉包中不同的使用方式依鸥。

調(diào)用步驟

動作 unowned weak
預(yù)先調(diào)用 #1 對象進行 unowned_retain 操作 創(chuàng)建一個容器亥至,并且對象進行 strong_retain 操作。創(chuàng)建一個可選值,存入到容器中抬闯,然后釋放可選值
預(yù)先調(diào)用 #2 strong_retain_unowned井辆,unowned_retain 和 strong_release strong_retain
閉包執(zhí)行 strong_retain_unowned,unowned_release load_weak, 打開可選值, strong_release
調(diào)用之后 unowned_release strong_release
  • unowned_retain:增加堆對象中的 unowned 引用計數(shù)溶握。
  • strong_retain_unowned :斷言對象的強引用計數(shù)大于 0杯缺,然后增加這個引用計數(shù)。
  • strong_retain:增加對象的強引用計數(shù)睡榆。
  • load_weak:不是真正的 ARC 調(diào)用萍肆,但是它將增加可選值指向?qū)ο蟮膹娨糜嫈?shù)。
  • strong_release:減少對象的強引用計數(shù)胀屿。如果釋放操作把對象強引用計數(shù)變?yōu)?塘揣,對象將被銷毀,然后弱引用將被清除宿崭。當(dāng)整個強引用計數(shù)和 unowned 引用計數(shù)都為0時亲铡,對象的內(nèi)存才會被釋放。
  • unowned_release:減少對象的 unowned 引用計數(shù)葡兑。當(dāng)整個強引用計數(shù)和 unowned 引用計數(shù)都為 0 時奖蔓,對象的內(nèi)存才會被釋放。

使用場景

  • unowned: 引用使用的場景是讹堤,原始實例永遠不會為 nil吆鹤,閉包可以直接使用它,并且直接定義為顯式解包可選值洲守。當(dāng)原始實例被析構(gòu)后疑务,在閉包中使用這個捕獲值將導(dǎo)致崩潰
  • 如果捕獲原始實例在使用過程中可能為 nil ,必須將引用聲明為 weak梗醇, 并且在使用之前驗證這個引用的有效性知允。

實現(xiàn)

unomned實現(xiàn)

來源于Swift5.0源碼HeapObject.cpp文件。

HeapObject *swift::swift_unownedRetain(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRetain);
  /// 檢測對象是否存在叙谨,不存在直接return
  if (!isValidPointerForNativeRetain(object))
    return object;

  /// 將對象的引用計數(shù)加1
  object->refCounts.incrementUnowned(1);
  return object;
}

void swift::swift_unownedRelease(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRelease);
  /// 檢測對象是否存在廊镜,不存在,直接return
  if (!isValidPointerForNativeRetain(object))
    return;

  // Only class objects can be unowned-retained and unowned-released.
  /// 檢測是否為類的對象
  assert(object->metadata->isClassObject());
  assert(static_cast<const ClassMetadata*>(object->metadata)->isTypeMetadata());
  
  /// 檢測Unowned引用計數(shù)是否能減1
  if (object->refCounts.decrementUnownedShouldFree(1)) {
    auto classMetadata = static_cast<const ClassMetadata*>(object->metadata);
    
    /// 釋放Unowned指針唉俗,并沒有釋放該指針指向的內(nèi)存
    swift_slowDealloc(object, classMetadata->getInstanceSize(),
                      classMetadata->getInstanceAlignMask());
  }
}

到此已經(jīng)有了一個對象的 unowned 引用嗤朴,另外一個指令,strong_retain_unowned 用來創(chuàng)建一個強引用:

HeapObject *swift::swift_unownedRetainStrong(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_unownedRetainStrong);
  if (!isValidPointerForNativeRetain(object))
    return object;
  /// 斷言來驗證對象是否被弱引用虫溜,一旦斷言通過雹姊,將嘗試進行增加強引用計數(shù)的操作.
  /// 一旦對象在進程中已經(jīng)被釋放,嘗試將會失敗衡楞。
  assert(object->refCounts.getUnownedCount() &&
         "object is not currently unowned-retained");

  /// 嘗試增加引用計數(shù)
  if (! object->refCounts.tryIncrement())
    /// 引用計數(shù)添加失敗
    swift::swift_abortRetainUnowned(object);
  return object;
}

weak

Swift5.0源碼

/// 初始化弱引用
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);
}

WeakReferenceBits(HeapObjectSideTableEntry *newValue) {
    setNativeOrNull(newValue);
}
/// 創(chuàng)建一個弱引用表吱雏,成功則增加弱引用計數(shù)敦姻。
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
/// 創(chuàng)建一個對象的散列表,如果該對象釋放了歧杏,則返回空
// Return an object's side table, allocating it if necessary.
// Returns null if the object is deiniting.
// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // Preflight failures before allocating a new side table.
  if (oldbits.hasSideTable()) {
    // Already have a side table. Return it.
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // Already past the start of deinit. Do nothing.
    return nullptr;
  }

  // Preflight passed. Allocate a side table.
  
  // FIXME: custom side table allocator
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      // Already have a side table. Return it and delete ours.
      // Read before delete to streamline barriers.
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      // Already past the start of deinit. Do nothing.
      return nullptr;
    }
    
    side->initRefCounts(oldbits);
    
    /// 進行 CAS
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}

增加引用計數(shù)

// Increment the weak reference count.
  void incrementWeak() {
    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));
  }
  
  bool decrementWeakShouldCleanUp() {
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    RefCountBits newbits;

    bool performFree;
    do {
      newbits = oldbits;
      performFree = newbits.decrementWeakRefCount();
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));

    return performFree;
  }

SideTable的數(shù)據(jù)結(jié)構(gòu)

class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;

  public:
  HeapObjectSideTableEntry(HeapObject *newObject)
    : object(newObject), refCounts()
  { }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Winvalid-offsetof"
  static ptrdiff_t refCountsOffset() {
    return offsetof(HeapObjectSideTableEntry, refCounts);
  }

弱引用的訪問:

HeapObject *nativeLoadStrong() {
    auto bits = nativeValue.load(std::memory_order_relaxed);
    return nativeLoadStrongFromBits(bits);
}

HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) {
    auto side = bits.getNativeOrNull();
    return side ? side->tryRetain() : nullptr;
}

到這里大家發(fā)現(xiàn)一個問題沒有镰惦,被引用對象釋放了為什么還能直接訪問 Side Table?其實 Swift ABI 中 Side Table 的生命周期與對象是分離的犬绒,當(dāng)強引用計數(shù)為 0 時旺入,只有 HeapObject 被釋放了。

只有所有的 weak 引用者都被釋放了或相關(guān)變量被置 nil 后凯力,Side Table 才能得以釋放茵瘾,詳見:

void decrementWeak() {
    // FIXME: assertions
    // FIXME: optimize barriers
    bool cleanup = refCounts.decrementWeakShouldCleanUp();
    if (!cleanup)
      return;

    // Weak ref count is now zero. Delete the side table entry.
    // FREED -> DEAD
    assert(refCounts.getUnownedCount() == 0);
    delete this;
  }

  void decrementWeakNonAtomic() {
    // FIXME: assertions
    // FIXME: optimize barriers
    bool cleanup = refCounts.decrementWeakShouldCleanUpNonAtomic();
    if (!cleanup)
      return;

    // Weak ref count is now zero. Delete the side table entry.
    // FREED -> DEAD
    assert(refCounts.getUnownedCount() == 0);
    delete this;
  }

所以即便使用了弱引用,也不能保證相關(guān)內(nèi)存全部被釋放咐鹤,因為只要 weak 變量不被顯式置 nil拗秘,Side Table 就會存在。而 ABI 中也有可以提升的地方祈惶,那就是如果訪問弱引用變量時發(fā)現(xiàn)被引用對象已經(jīng)釋放雕旨,就將自己的弱引用銷毀掉,避免之后重復(fù)無意義的 CAS 操作捧请。當(dāng)然 ABI 不做這個優(yōu)化凡涩,我們也可以在 Swift 代碼里做。

以上就是Swift weak 弱引用機制實現(xiàn)方式的一個簡單的分析血久,可見思路與 Objective-C runtime 還是很類似的突照,都采用與對象匹配的 Side Table 來維護引用計數(shù)帮非。不同的地方就是 Objective-C 對象在內(nèi)存布局中沒有 Side Table 指針氧吐,而是通過一個全局的 StripedMap 來維護對象和 Side Table 之間的關(guān)系,效率沒有 Swift 這么高末盔。另外 Objective-C runtime 在對象釋放時會將所有的 __weak 變量都 zero-out筑舅,而 Swift 并沒有

小結(jié)

在這個實現(xiàn)中,獲取一個強引用需要更多復(fù)雜同步操作陨舱,在多線程競爭嚴(yán)重的情況下翠拣,會帶來性能損耗

結(jié)論

  • 保守的使用 weak 引用是否明智呢?答案是否定的游盲,無論是從性能的角度還是代碼清晰的角度而言误墓。
  • 使用正確的捕獲修飾符類型,明確的表明代碼中的生命周期特性益缎,當(dāng)其他人或者你自己在讀你的代碼時不容易誤解
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谜慌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子莺奔,更是在濱河造成了極大的恐慌欣范,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恼琼,居然都是意外死亡妨蛹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門晴竞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛙卤,“玉大人,你說我怎么就攤上這事颓鲜”砭剑” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵甜滨,是天一觀的道長乐严。 經(jīng)常有香客問我,道長衣摩,這世上最難降的妖魔是什么昂验? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮艾扮,結(jié)果婚禮上既琴,老公的妹妹穿的比我還像新娘。我一直安慰自己泡嘴,他們只是感情好甫恩,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酌予,像睡著了一般磺箕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抛虫,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天松靡,我揣著相機與錄音,去河邊找鬼建椰。 笑死雕欺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的棉姐。 我是一名探鬼主播屠列,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伞矩!你這毒婦竟也來了笛洛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤扭吁,失蹤者是張志新(化名)和其女友劉穎撞蜂,沒想到半個月后盲镶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡蝌诡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年溉贿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浦旱。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡宇色,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出颁湖,到底是詐尸還是另有隱情宣蠕,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布甥捺,位于F島的核電站抢蚀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏镰禾。R本人自食惡果不足惜皿曲,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吴侦。 院中可真熱鬧屋休,春花似錦、人聲如沸备韧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽织堂。三九已至叠艳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捧挺,已是汗流浹背虑绵。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工尿瞭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留闽烙,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓声搁,卻偏偏與公主長得像黑竞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子疏旨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

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