序言:各個社群有關 Objective-C weak 機制的實現分析文章有很多,然而 Swift 釋出這麼長時間以來,有關 ABI 的分析文章一直非常少,似乎也是很多 iOS 開發者未涉及的領域… 本文就從原始碼層面分析一下 Swift 是如何實現 weak 機制的。
準備工作
由於 Swift 原始碼量較大,強烈建議大家把 repo clone 下來,結合原始碼一起來看這篇文章。
$ 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 中的物件(class
例項)它的記憶體佈局是怎樣的。
HeapObject
我們知道 Objective-C 在 runtime 中通過 objc_object
來表示一個物件,這些型別定義了物件在記憶體中頭部的結構。同樣的,在 Swift 中也有類似的結構,那就是 HeapObject
,我們來看一下它的定義:
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 在很多情況下並不會用到它,比如靜態方法派發等等。
接下來是 SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS
,這是一個巨集定義,展開後即:
RefCounts<InlineRefCountBits> refCounts;
複製程式碼
這是一個相當重要東西,引用計數、弱引用、unowned 引用都與它有關,同時它也是 Swift 物件(文中後續的 Swift 物件均指引用型別,即 class
的例項)中較為複雜的一個結構。
其實說複雜也並不是很複雜,我們知道 Objective-C runtime 裡就有很多 union 結構的應用,例如 isa_t
有 pointer 型別也有 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:
- 呼叫 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_;
複製程式碼
- 呼叫
RefCounts
的increment
方法:
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 引用會怎樣。
- 呼叫 SIL 介面
swift::swift_weakAssign
(暫時省略這塊的邏輯,它屬於引用者的邏輯,我們現在先分析被引用者) - 呼叫
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,本文僅供參考,如果存在錯誤,歡迎大家勘正。感謝!