從源碼解析 Swift 弱引用

序言:各個社區有關 Objective-C weak 機制的實現分析文章有不少,然而 Swift 發佈這麼長時間以來,有關 ABI 的分析文章一直很是少,彷佛也是不少 iOS 開發者未涉及的領域… 本文就從源碼層面分析一下 Swift 是如何實現 weak 機制的。git

準備工做

因爲 Swift 源碼量較大,強烈建議你們把 repo clone 下來,結合源碼一塊兒來看這篇文章。github

$ git clone https://github.com/apple/swift.git
複製代碼

Swift 整個工程採用了 CMake 做爲構建工具,若是你想用 Xcode 來打開的話須要先安裝 LLVM,而後用 cmake -G 生成 Xcode 項目。編程

咱們這裏只是進行源碼分析,我就直接用 Visual Studio Code 配合 C/C++ 插件了,一樣支持符號跳轉、查找引用。另外提醒一下你們,Swift stdlib 裏 C++ 代碼的類型層次比較複雜,不使用 IDE 輔助閱讀起來會至關費勁。swift

正文

下面咱們就正式進入源碼分析階段,首先咱們來看一下 Swift 中的對象(class 實例)它的內存佈局是怎樣的。安全

HeapObject

咱們知道 Objective-C 在 runtime 中經過 objc_object 來表示一個對象,這些類型定義了對象在內存中頭部的結構。一樣的,在 Swift 中也有相似的結構,那就是 HeapObject,咱們來看一下它的定義:bash

struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  
  // Initialize a HeapObject header for an immortal object
  constexpr HeapObject(HeapMetadata const *newMetadata,
                       InlineRefCounts::Immortal_t immortal)
  : metadata(newMetadata)
  , refCounts(InlineRefCounts::Immortal)
  { }

};
複製代碼

能夠看到,HeapObject 的第一個字段是一個 HeapMetadata 對象,這個對象有着與 isa_t 相似的做用,就是用來描述對象類型的(等價於 type(of:) 取得的結果),只不過 Swift 在不少狀況下並不會用到它,好比靜態方法派發等等。app

接下來是 SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS,這是一個宏定義,展開後即:ide

RefCounts<InlineRefCountBits> refCounts;
複製代碼

這是一個至關重要東西,引用計數、弱引用、unowned 引用都與它有關,同時它也是 Swift 對象(文中後續的 Swift 對象均指引用類型,即 class 的實例)中較爲複雜的一個結構。函數

其實說複雜也並非很複雜,咱們知道 Objective-C runtime 裏就有不少 union 結構的應用,例如 isa_tpointer 類型也有 nonpointer 類型,它們都佔用了相同的內存空間,這樣作的好處就是能更高效地使用內存,尤爲是這些大量使用到的東西,能夠大大減小運行期的開銷。相似的技術在 JVM 裏也有,就如對象頭的 mark word。固然,Swift ABI 中也大量採用這種技術。工具

RefCounts 類型和 Side Table

上面說到 RefCounts 類型,這裏咱們就來看看它究竟是個什麼東西。

先看一下定義:

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  
  // ...
  
};
複製代碼

這就是 RefCounts 的內存佈局,我這裏省略了全部的方法和類型定義。你能夠把 RefCounts 想象成一個線程安全的 wrapper,模板參數 RefCountBits 指定了真實的內部類型,在 Swift ABI 裏總共有兩種:

typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
複製代碼

前者是用在 HeapObject 中的,然後者是用在 HeapObjectSideTableEntry(Side Table)中的,這兩種類型後文我會一一講到。

通常來說,Swift 對象並不會用到 Side Table,一旦對象被 weak 或 unowned 引用,該對象就會分配一個 Side Table。

InlineRefCountBits

定義:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {

  friend class RefCountBitsT<RefCountIsInline>;
  friend class RefCountBitsT<RefCountNotInline>;
  
  static const RefCountInlinedness Inlinedness = refcountIsInline;

  typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;
  typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::SignedType
    SignedBitsType;
  typedef RefCountBitOffsets<sizeof(BitsType)>
    Offsets;

  BitsType bits;
  
  // ...
  
};
複製代碼

經過模板替換以後,InlineRefCountBits 實際上就是一個 uint64_t,相關的一堆類型就是爲了經過模板元編程讓代碼可讀性更高(或者更低,哈哈哈)。

下面咱們來模擬一下對象引用計數 +1:

  1. 調用 SIL 接口 swift::swift_retain
HeapObject *swift::swift_retain(HeapObject *object) {
  return _swift_retain(object);
}

static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    object->refCounts.increment(1);
  return object;
}

auto swift::_swift_retain = _swift_retain_;
複製代碼
  1. 調用 RefCountsincrement 方法:
void increment(uint32_t inc = 1) {
  // 3. 原子地讀出 InlineRefCountBits 對象(即一個 uint64_t)。
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  RefCountBits newbits;
  do {
    newbits = oldbits;
    // 4. 調用 InlineRefCountBits 的 incrementStrongExtraRefCount 方法
    // 對這個 uint64_t 進行一系列運算。
    bool fast = newbits.incrementStrongExtraRefCount(inc);
    // 無 weak、unowned 引用時通常不會進入。
    if (SWIFT_UNLIKELY(!fast)) {
      if (oldbits.isImmortal())
        return;
      return incrementSlow(oldbits, inc);
    }
    // 5. 經過 CAS 將運算後的 uint64_t 設置回去。
  } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                            std::memory_order_relaxed));
}
複製代碼

到這裏就完成了一次 retain 操做。

SideTableRefCountBits

上面是不存在 weak、unowned 引用的狀況,如今咱們來看看增長一個 weak 引用會怎樣。

  1. 調用 SIL 接口 swift::swift_weakAssign(暫時省略這塊的邏輯,它屬於引用者的邏輯,咱們如今先分析被引用者)
  2. 調用 RefCounts<InlineRefCountBits>::formWeakReference 增長一個弱引用:
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  // 分配一個 Side Table。
  auto side = allocateSideTable(true);
  if (side)
    // 增長一個弱引用。
    return side->incrementWeak();
  else
    return nullptr;
}
複製代碼

重點來看一下 allocateSideTable 的實現:

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // 已有 Side Table 或正在析構就直接返回。
  if (oldbits.hasSideTable()) {
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    return nullptr;
  }

  // 分配 Side Table 對象。
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      // 此時可能其餘線程建立了 Side Table,刪除該線程分配的,而後返回。
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      return nullptr;
    }
    
    // 用當前的 InlineRefCountBits 初始化 Side Table。
    side->initRefCounts(oldbits);
    // 進行 CAS。
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}
複製代碼

還記得 HeapObject 裏的 RefCounts 其實是 InlineRefCountBits 的一個 wrapper 嗎?上面構造完 Side Table 之後,對象中的 InlineRefCountBits 就不是原來的引用計數了,而是一個指向 Side Table 的指針,然而因爲它們實際都是 uint64_t,所以須要一個方法來區分。區分的方法咱們能夠來看 InlineRefCountBits 的構造函數:

LLVM_ATTRIBUTE_ALWAYS_INLINE
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }
複製代碼

其實仍是最多見的方法,把指針地址無用的位替換成標識位。

順便,看一下 Side Table 的結構:

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

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

  // ...

};
複製代碼

此時再增長引用計數會怎樣呢?來看下以前的 RefCounts::increment 方法:

void increment(uint32_t inc = 1) {
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  RefCountBits newbits;
  do {
    newbits = oldbits;
    bool fast = newbits.incrementStrongExtraRefCount(inc);
    // ---> 此次進入這個分支。
    if (SWIFT_UNLIKELY(!fast)) {
      if (oldbits.isImmortal())
        return;
      return incrementSlow(oldbits, inc);
    }
  } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                            std::memory_order_relaxed));
}
複製代碼
template <typename RefCountBits>
void RefCounts<RefCountBits>::incrementSlow(RefCountBits oldbits,
                                            uint32_t n) {
  if (oldbits.isImmortal()) {
    return;
  }
  else if (oldbits.hasSideTable()) {
    auto side = oldbits.getSideTable();
    // ---> 而後調用到這裏。
    side->incrementStrong(n);
  }
  else {
    swift::swift_abortRetainOverflow();
  }
}
複製代碼
void HeapObjectSideTableEntry::incrementStrong(uint32_t inc) {
  // 最終到這裏,refCounts 是一個 RefCounts<SideTableRefCountBits> 對象。
  refCounts.increment(inc);
}
複製代碼

到這裏咱們就須要引出 SideTableRefCountBits 了,它與前面的 InlineRefCountBits 很像,只不過又多了一個字段,看一下定義:

class SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits;
  
  // ...
  
};
複製代碼

小結一下

不知道上面的內容你們看暈了沒有,反正我一開始分析的時候費了點時間。

上面咱們講了兩種 RefCounts,一種是 inline 的,用在 HeapObject 中,它實際上是一個 uint64_t,能夠當引用計數也能夠當 Side Table 的指針。

Side Table 是一種類名爲 HeapObjectSideTableEntry 的結構,裏面也有 RefCounts 成員,是內部是 SideTableRefCountBits,其實就是原來的 uint64_t 加上一個存儲弱引用數的 uint32_t

WeakReference

上面說的都是被引用的對象所涉及的邏輯,而引用者這邊的邏輯就稍微簡單一些了,主要就是經過 WeakReference 這個類來實現的,比較簡單,咱們簡單過一下就行。

Swift 中的 weak 變量通過 silgen 以後都會變成 swift::swift_weakAssign 調用,而後派發給 WeakReference::nativeAssign

void nativeAssign(HeapObject *newObject) {
  if (newObject) {
    assert(objectUsesNativeSwiftReferenceCounting(newObject) &&
           "weak assign native with non-native new object");
  }
  
  // 讓被引用者構造 Side Table。
  auto newSide =
    newObject ? newObject->refCounts.formWeakReference() : nullptr;
  auto newBits = WeakReferenceBits(newSide);

  // 喜聞樂見的 CAS。
  auto oldBits = nativeValue.load(std::memory_order_relaxed);
  nativeValue.store(newBits, std::memory_order_relaxed);

  assert(oldBits.isNativeOrNull() &&
         "weak assign native with non-native old object");
  // 銷燬原來對象的弱引用。
  destroyOldNativeBits(oldBits);
}
複製代碼

弱引用的訪問就更簡單了:

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

到這裏你們發現一個問題沒有,被引用對象釋放了爲何還能直接訪問 Side Table?其實 Swift ABI 中 Side Table 的生命週期與對象是分離的,當強引用計數爲 0 時,只有 HeapObject 被釋放了。

只有全部的 weak 引用者都被釋放了或相關變量被置 nil 後,Side Table 才能得以釋放,相見:

void HeapObjectSideTableEntry::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;
}
複製代碼

因此即使使用了弱引用,也不能保證相關內存所有被釋放,由於只要 weak 變量不被顯式置 nil,Side Table 就會存在。而 ABI 中也有能夠提高的地方,那就是若是訪問弱引用變量時發現被引用對象已經釋放,就將本身的弱引用銷燬掉,避免以後重複無心義的 CAS 操做。固然 ABI 不作這個優化,咱們也能夠在 Swift 代碼裏作。:)

總結

以上就是 Swift 弱引用機制實現方式的一個簡單的分析,可見思路與 Objective-C runtime 仍是很相似的,都採用與對象匹配的 Side Table 來維護引用計數。不一樣的地方就是 Objective-C 對象在內存佈局中沒有 Side Table 指針,而是經過一個全局的 StripedMap 來維護對象和 Side Table 之間的關係,效率沒有 Swift 這麼高。另外 Objective-C runtime 在對象釋放時會將全部的 __weak 變量都 zero-out,而 Swift 並無。

總的來講,Swift 的實現方式會稍微簡單一些(雖然代碼更復雜,Swift 團隊追求更高的抽象)。第一次分析 Swift ABI,本文僅供參考,若是存在錯誤,歡迎你們勘正。感謝!

相關文章
相關標籤/搜索