Friday Q&A 2015-12-11:Swift 中的弱引用

作者:Mike Ash精续,原文鏈接挠唆,原文日期:2015-12-11
譯者:riven;校對:Cee雹仿;定稿:千葉知風

即便你已經(jīng)在火星的一個洞穴里增热,緊閉著你的雙眼并且捂住你的耳朵,也避免不了 Swift 已經(jīng)開源的事實胧辽。正因為開源峻仇,我們能夠更加方便地去探索 Swift 中的很多有趣的特性,其中之一便是 Swift 中弱引用是如何工作的問題邑商。

弱引用

在采用垃圾回收器或者引用計數(shù)進行內(nèi)存管理的語言中摄咆,強引用可以使得特定的對象一直存活,但弱引用就無法保證人断。當一個對象被強引用時吭从,它是不能夠被銷毀的;但是如果它是個弱引用恶迈,就可以涩金。

當我們所提到「弱引用」時,通常的意思是指一個歸零弱引用(Zeroing Weak Reference)暇仲。也就是說步做,當弱引用的目標對象被銷毀時,弱引用就會變成 nil(校者注:看這篇文章了解更多)奈附。非歸零弱引用也是存在的全度,它會導致一些陷阱(Trap)、崩潰(Crash)或者未定義行為的調(diào)用斥滤。比如你在 Objective-C 中使用 unsafe_unretained将鸵,或者在 Swift 中使用 unowned(Objective-C 未定義行為處理方式,而 Swift 卻很可靠地處理這些崩潰)佑颇。

歸零弱引用很方便使用顶掉,在基于引用計數(shù)進行內(nèi)存管理的語言中他們是非常有用的。它們允許循環(huán)引用存在卻不會產(chǎn)生死循環(huán)漩符,并且不需要手動打破逆向引用一喘。他們非常的有用驱还,在蘋果引入 ARC 和讓弱引用在垃圾收集代碼之外的語言層面上可用之前嗜暴,我就已經(jīng)實現(xiàn)了我自己的弱引用版本

它是如何工作的呢议蟆?

歸零弱引用比較典型的實現(xiàn)方式是保持一個對每個對象的所有弱引用列表闷沥。當對一個對象創(chuàng)建了弱引用,這個引用就會被添加到這個列表中咐容。當這個引用被重新賦值或者超出了其作用域舆逃,它就會從列表中被移除。當一個對象被銷毀,這個列表中的所有引用都會被歸零路狮。在多線程的情況下虫啥,其實現(xiàn)必須是同步獲取一個弱引用并銷毀一個對象,以避免競態(tài)條件的出現(xiàn):比如當一個線程釋放某個對象的最后一個強引用而同時另一個線程卻試圖加載一個它的一個弱引用奄妨。

在我的實現(xiàn)中涂籽,每一個弱引用都是一個完整的對象。弱引用列表是一個弱引用對象的集合砸抛。雖然由于額外的轉(zhuǎn)換和內(nèi)存使用讓效率變低了评雌,但這種方式可以很方便的讓這些引用變成完整的對象。

蘋果公司的 Objective-C 的實現(xiàn)是這樣的直焙,每一個弱引用是一個指向目標對象的普通指針景东。編譯器并不直接讀寫指針,而是使用一些幫助函數(shù)奔誓。當存儲一個弱指針時斤吐,存儲函數(shù)會將指針的位置注冊為目標對象的一個弱引用。由于讀取函數(shù)被集成進了引用計數(shù)系統(tǒng)厨喂,這就確保了在讀取一個弱指針時曲初,不會返回一個已經(jīng)被釋放了的對象的指針。

歸零操作

讓我們創(chuàng)建一些代碼來研究一下它們究竟是怎么運行的杯聚。

我們希望寫一個函數(shù)能夠 dump 一個對象的內(nèi)存內(nèi)容臼婆。這個函數(shù)接受一塊內(nèi)存區(qū)域,將其按指針大小進行分塊幌绍,并且將最終的結(jié)果轉(zhuǎn)換成一個易于查看的十六進制字符串:

func contents(ptr: UnsafePointer<Void>, _ length: Int) -> String {
    let wordPtr = UnsafePointer<UInt>(ptr)
    let words = length / sizeof(UInt.self)
    let wordChars = sizeof(UInt.self) * 2

    let buffer = UnsafeBufferPointer<UInt>(start: wordPtr, count: words)
    let wordStrings = buffer.map({ word -> String in
        var wordString = String(word, radix: 16)
        while wordString.characters.count < wordChars {
            wordString = "0" + wordString
        }
        return wordString
    })
    return wordStrings.joinWithSeparator(" ")
}

下一個函數(shù)會為一個對象創(chuàng)建一個 dump 函數(shù)颁褂。調(diào)用時傳入一個對象,它會返回一個 dump 這個對象內(nèi)容的函數(shù)傀广。在函數(shù)內(nèi)部颁独,我們給對象保存了一個 UnsafePointer,而不是普通的引用伪冰。這樣可以確保它不會和語言的引用計數(shù)系統(tǒng)發(fā)生交互誓酒。它允許我們可以在這個對象被銷毀之后 dump 出它的內(nèi)存,后面我們會介紹贮聂。

func dumperFunc(obj: AnyObject) -> (Void -> String) {
    let objString = String(obj)
    let ptr = unsafeBitCast(obj, UnsafePointer<Void>.self)
    let length = class_getInstanceSize(obj.dynamicType)
    return {
        let bytes = contents(ptr, length)
        return "\(objString) \(ptr): \(bytes)"
    }
}

下面是一個包含弱引用變量的類靠柑,后面我會觀察這個弱引用。我在弱引用變量的前后分別添加了一個 dummy 變量吓懈,以便于我們區(qū)分弱引用在 dump 出來的內(nèi)存結(jié)構(gòu)中的位置:

class WeakReferer {
    var dummy1 = 0x1234321012343210
    weak var target: WeakTarget?
    var dummy2: UInt = 0xabcdefabcdefabcd
}

讓我們試一下! 我們先創(chuàng)建一個引用歼冰,然后 dump 它:

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())

打印結(jié)果:

WeakReferer 0x00007f8a3861b920: 0000000107ab24a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

我們看到 isa 指針位于最開始的位置,緊隨其后的是其它一些內(nèi)部字段耻警。dummy1 變量占據(jù)了第四塊隔嫡,dummy2 變量占據(jù)了第六塊甸怕。正如我們所期望的那樣,在他們之間的弱引用正好是零腮恩。

現(xiàn)在我們讓這個弱引用指向一個目標對象梢杭,看看會變成什么樣。我將這段代碼放入一個 do語句中秸滴,以便于當目標對象超出作用域和被銷毀時我們可以進行控制:

do {
    let target = NSObject()
    referer.target = target
    print(target)
    print(refererDump())
}

打印結(jié)果:

<NSObject: 0x7fda6a21c6a0>
WeakReferer 0x00007fda6a000ad0: 00000001050a44a0 0000000200000004 1234321012343210 00007fda6a21c6a0 abcdefabcdefabcd

正如我們期望的那樣式曲,目標對象的指針直接存儲在弱引用中。在目標對象被銷毀之后缸榛,我們在 do 代碼塊之后再次調(diào)用 dump 函數(shù):

print(refererDump())

WeakReferer 0x00007ffe32300060: 000000010cfb44a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

它被歸零了吝羞。點個贊!

僅僅為了好玩,我們用一個純 Swift 對象作為對象來重復這個實驗内颗。不必要時钧排,我并不是很想使用 Objective-C 中的東西。下面是一個純 Swift 對象:

class WeakTarget {}

讓我們試一下:

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
do {
    class WeakTarget {}
    let target = WeakTarget()
    referer.target = target
    print(refererDump())
}
print(refererDump())

目標對象像我們期望的那樣被歸零了均澳,然后被重新賦值:

WeakReferer 0x00007fbe95000270: 00000001071d24a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007fbe95000270: 00000001071d24a0 0000000200000004 1234321012343210 00007fbe95121ce0 abcdefabcdefabcd

然后當目標對象被銷毀恨溜,引用應該被歸零:

WeakReferer 0x00007fbe95000270: 00000001071d24a0 0000000200000004 1234321012343210 00007fbe95121ce0 abcdefabcdefabcd

不幸的是它并沒有被歸零≌仪埃可能是目標對象沒有被銷毀糟袁。一定是有某些東西讓它繼續(xù)活著!讓我們再檢查一下:

class WeakTarget {
    deinit { print("WeakTarget deinit") }
}

再次運行代碼躺盛,結(jié)果如下:

WeakReferer 0x00007fd29a61fa10: 0000000107ae44a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007fd29a61fa10: 0000000107ae44a0 0000000200000004 1234321012343210 00007fd29a42a920 abcdefabcdefabcd
WeakTarget deinit
WeakReferer 0x00007fd29a61fa10: 0000000107ae44a0 0000000200000004 1234321012343210 00007fd29a42a920 abcdefabcdefabcd

它消失了项戴,但是弱引用并沒有歸零。怎么回事呢槽惫,我們發(fā)現(xiàn)了 Swift 的一個 bug周叮!很神奇,這個 bug 一直沒有被解決界斜。你會想之前肯定已經(jīng)有人已經(jīng)注意到了這個問題仿耽。接下來,我們通過訪問弱引用來產(chǎn)生一個崩潰各薇,然后我們可以用這個 Swift 工程提交這個 bug :

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
do {
    class WeakTarget {
        deinit { print("WeakTarget deinit") }
    }
    let target = WeakTarget()
    referer.target = target
    print(refererDump())
}
print(refererDump())
print(referer.target)

下面就是崩潰信息:

WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 00007ff7aa2157f0 abcdefabcdefabcd
WeakTarget deinit
WeakReferer 0x00007ff7aa20d060: 00000001047a04a0 0000000200000004 1234321012343210 00007ff7aa2157f0 abcdefabcdefabcd
nil

哦项贺,我的天吶!大爆炸在哪呢峭判?應該有一個驚天動地的大爆炸呀开缎!輸出的內(nèi)容表明一切工作正常,但我們可以清楚地從 dump 內(nèi)容看到它并沒有正常工作朝抖。

讓我們再仔細檢查一下啥箭。下面是一個經(jīng)過修改的 WeakTarget 類谍珊,我們添加了一個 dummy 變量以便于區(qū)分 dump 的內(nèi)容:

class WeakTarget {
    var dummy = 0x0123456789abcdef

    deinit {
        print("Weak target deinit")
    }
}

下面是一段新的代碼治宣,運行的程序和之前的基本相同急侥,只不過每次 dump 都會輸出兩個對象(校者注:Target 和 Referer):

let referer = WeakReferer()
let refererDump = dumperFunc(referer)
print(refererDump())
let targetDump: Void -> String
do {
    let target = WeakTarget()
    targetDump = dumperFunc(target)
    print(targetDump())

    referer.target = target

    print(refererDump())
    print(targetDump())
}
print(refererDump())
print(targetDump())
print(referer.target)
print(refererDump())
print(targetDump())

讓我們檢查一下輸出內(nèi)容。referer 對象的生命周期和之前一樣侮邀,它的 target 字段被順利的歸零了:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

target 首先作為一個普通對象坏怪,在各種頭字段之后緊跟著我們的 dummy 字段:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000004 0123456789abcdef

在給 target 字段賦值后,我們可以看到被填充的指針的值:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 00007fe17341d270 abcdefabcdefabcd

target 對象還是和之前一樣绊茧,但是它其中一個頭字段增加了 2:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000400000004 0123456789abcdef

目標對象像我們期望的那樣被銷毀了:

Weak target deinit

我們看到引用對象一直都有一個指針指向目標對象:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 00007fe17341d270 abcdefabcdefabcd

并且目標對象本身一直存活著铝宵。和上次我們看到的相比,它的頭字段減少了 2:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000002 0123456789abcdef

訪問 target 字段會產(chǎn)生 nil 华畏,即便它沒有被歸零:

nil

再次 dump referer 對象的內(nèi)容鹏秋,從中我們看出僅僅訪問 target 字段的行為已經(jīng)改變了它。現(xiàn)在它被歸零了:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

目標對象現(xiàn)在被完全抹掉了:

WeakTarget 0x00007fe17341d270: 200007fe17342a04 300007fe17342811 ffffffffffff0002

現(xiàn)在變的越來越有趣了亡笑。我們看到頭字段會一會兒增加侣夷,一會兒減少;讓我們看看是否能有重現(xiàn)出更多的信息:

let target = WeakTarget()
let targetDump = dumperFunc(target)
do {
    print(targetDump())
    weak var a = target
    print(targetDump())
    weak var b = target
    print(targetDump())
    weak var c = target
    print(targetDump())
    weak var d = target
    print(targetDump())
    weak var e = target
    print(targetDump())

    var f = target
    print(targetDump())
    var g = target
    print(targetDump())
    var h = target
    print(targetDump())
    var i = target
    print(targetDump())
    var j = target
    print(targetDump())
    var k = target
    print(targetDump())
}
print(targetDump())

打印結(jié)果:

WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000200000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000400000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000600000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000800000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000a00000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000004 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000008 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c0000000c 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000010 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000014 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c00000018 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000c0000001c 0123456789abcdef
WeakTarget 0x00007fd883205df0: 00000001093a4840 0000000200000004 0123456789abcdef

我們看到每一個新的弱引用會讓頭字段中的第一個數(shù)增加 2仑乌。每一個新的強引用會讓頭字段中的第二個數(shù)增加 4百拓。

回顧一下,下面這些就是目前我們所發(fā)現(xiàn)的:

  • 在內(nèi)存中弱指針和普通指針是一樣的.
  • 當一個弱目標對象(WeakTarget)的 deinit 方法調(diào)用時晰甚,目標對象是不會被釋放的衙传,并且弱指針也不會被歸零。
  • 當目標對象的 deinit 方法執(zhí)行之后厕九,訪問弱指針蓖捶,它就會被歸零并且弱目標對象也會被釋放。
  • 弱目標對象包含一個弱引用的引用計數(shù)扁远,與強引用計數(shù)分離開腺阳。

Swift 代碼

既然 Swift 已經(jīng)開源,我們可以通過查看源代碼來繼續(xù)我們的觀察穿香。

在 Swift 標準庫中用 HeapObject 類型來表示一個分配在堆上的對象亭引,其實現(xiàn)可參考 stdlib/public/SwiftShims/HeapObject.h∑せ瘢看起來是這樣的:

cpp
struct HeapObject {
/// 這始終是一個有效的元數(shù)據(jù)對象的指針焙蚓。
struct HeapMetadata const *metadata;

SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
// FIXME: 在 32 位的平臺上分配了兩個字大小的元數(shù)據(jù)。

#ifdef __cplusplus
HeapObject() = default;

// 給新分配的堆內(nèi)存初始化空間(對象alloc洒宝,是分配的堆內(nèi)存)购公。
constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCount(StrongRefCount::Initialized)
    , weakRefCount(WeakRefCount::Initialized)
{ }
#endif
};

Swift 的 metadata 字段就相當于 Objective-C 的 isa 字段,并且它們是兼容的雁歌。還有一些像 NON_OBJC_MEMBERS 這樣的宏定義:

cpp
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  StrongRefCount refCount;                      \
  WeakRefCount weakRefCount

噢宏浩,快看!這就是我們的兩個引用計數(shù)靠瞎。

(附加問題:為什么這里強引用在前面比庄,而在 dump 時確是弱引用在前面求妹?)

引用計數(shù)是通過位于 stdlib/public/runtime/HeapObject.cpp 文件中的一系列函數(shù)來進行管理的。比如,下面的 swift_retain

cpp
void swift::swift_retain(HeapObject *object) {
SWIFT_RETAIN();
    _swift_retain(object);
}
static void _swift_retain_(HeapObject *object) {
    _swift_retain_inlined(object);
}
auto swift::_swift_retain = _swift_retain_;

這里面拐了幾個彎,但它最終是調(diào)用頭文件中的內(nèi)聯(lián)函數(shù):

cpp
static inline void _swift_retain_inlined(HeapObject *object) {
  if (object) {
    object->refCount.increment();
  }
}

如你所見记焊,它會增加引用計數(shù)愉老。下面是 increment 函數(shù)的實現(xiàn):

cpp
void increment() {
  __atomic_fetch_add(&refCount, RC_ONE, __ATOMIC_RELAXED);
}

RC_ONE 來自于一個枚舉類型:

cpp
enum : uint32_t {
  RC_PINNED_FLAG = 0x1,
  RC_DEALLOCATING_FLAG = 0x2,

  RC_FLAGS_COUNT = 2,
  RC_FLAGS_MASK = 3,
  RC_COUNT_MASK = ~RC_FLAGS_MASK,

  RC_ONE = RC_FLAGS_MASK + 1
};

相信你已經(jīng)明白為什么每一個新的強引用會讓頭字段增加 4 了吧。這個枚舉類型的前兩位用來作為標志位。回想一下之前的 dump 結(jié)果,我們可以看到這些標志位鹃唯。下面是一個弱目標對象在最后一個強引用消失之前和之后的結(jié)果:

WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000400000004 0123456789abcdef
Weak target deinit
WeakTarget 0x00007fe17341d270: 000000010faa63e0 0000000200000002 0123456789abcdef

其中第二個字段開始是 4,表示引用計數(shù)為 1 并且沒有標志位瓣喊,之后變成了 2俯渤,表示引用計數(shù)為 0 和 RC_DEALLOCATING_FLAG 標志位被設定了。這個被析構(gòu)的對象被放在了處于 DEALLOCATING 狀態(tài)的位置型宝。

(順便說一句八匠,RC_PINNED_FLAG 到底是用來干什么的呢?我查找了相關代碼趴酣,除了能夠表明一個「固定的對象(pinned object)」外梨树,其它對于這個標記一無所知。如果你弄清楚了或者有一些相關的猜測岖寞,請給我留言抡四。)

現(xiàn)在讓我們看一看弱引用計數(shù)的實現(xiàn)。它有同樣的枚舉結(jié)構(gòu):

cpp
enum : uint32_t {
  // There isn't really a flag here.
  // Making weak RC_ONE == strong RC_ONE saves an
  // instruction in allocation on arm64.
  RC_UNUSED_FLAG = 1,

  RC_FLAGS_COUNT = 1,
  RC_FLAGS_MASK = 1,
  RC_COUNT_MASK = ~RC_FLAGS_MASK,

  RC_ONE = RC_FLAGS_MASK + 1
};

這就是 2 的來源:其中有一個保留的標志位仗谆,目前尚未被使用指巡。奇怪的是,關于這段代碼的注釋似乎是不正確的隶垮,這的 RC_ONE 等于 2藻雪,而強引用的 RC_ONE 等于 4。我猜它們曾經(jīng)是相等的狸吞,但后來它被修改了而注釋卻沒有更新勉耀。我只是想表明如果注釋是無用的,那你為什么還要寫它呢蹋偏。

所有這些是如何和加載弱引用相關聯(lián)的呢便斥?它是由 swift_weakLoadStrong 函數(shù)來處理的:

cpp
HeapObject *swift::swift_weakLoadStrong(WeakReference *ref) {
  auto object = ref->Value;
  if (object == nullptr) return nullptr;
  if (object->refCount.isDeallocating()) {
    swift_weakRelease(object);
    ref->Value = nullptr;
    return nullptr;
  }
  return swift_tryRetain(object);
}

從上面的代碼,惰性歸零是如何工作的已經(jīng)一目了然了威始。當加載一個弱引用時枢纠,如果目標對象正在被銷毀,就會對這個引用進行歸零黎棠。反之晋渺,會保留目標對象并返回它镰绎。進一步深挖一點,我們可以看到 swift_weakRelease 如何釋放對象的內(nèi)存些举,前提是它是最后一個引用:

cpp
void swift::swift_weakRelease(HeapObject *object) {
  if (!object) return;

  if (object->weakRefCount.decrementShouldDeallocate()) {
    // 只有對象可以 weak-retained 和 weak-released
    auto metadata = object->metadata;
    assert(metadata->isClassObject());
    auto classMetadata = static_cast<const ClassMetadata*>(metadata);
    assert(classMetadata->isTypeMetadata());
    swift_slowDealloc(object, classMetadata->getInstanceSize(),
                      classMetadata->getInstanceAlignMask());
  }
}

(注意:如果你正在查看版本庫中的代碼跟狱,使用「weak」命名的地方大多數(shù)都改成了「unowned」俭厚。上面的命名是截至撰寫本文時最新的快照户魏,但開發(fā)仍在繼續(xù)。你可以查看和我這對應的版本庫中的 2.2 版本的快照挪挤,或者獲取最新的版本但是要注意命名的變化叼丑,并且實現(xiàn)也有可能發(fā)生了改變。)

整合

我們已經(jīng)在層級上自上往下地看到了 Swift 中的弱引用是如何實現(xiàn)的扛门。那么在高層觀察 Swift 的弱引用又是如何工作的呢鸠信?

  1. 弱引用只是指向目標對象的指針。
  2. 在 Objective-C 中是沒有辦法單獨追蹤弱引用的论寨。
  3. 相反星立,每一個 Swift 對象都有一個弱引用計數(shù),和它的強引用計數(shù)相鄰葬凳。
  4. Swift 將對象的析構(gòu)過程(deinit)和對象的釋放(dealloc)解耦绰垂。一個對象可以被析構(gòu)并釋放它的外部資源,但不必釋放對象本身所占用的內(nèi)存火焰。
  5. 當一個 Swift 對象的強引用計數(shù)變成零而弱引用計數(shù)仍大于零時劲装,那么這個對象會被析構(gòu),但是不會被釋放昌简。
  6. 這意味著一個被釋放對象的弱指針仍然是一個有效的指針占业,它可以被反向引用而不會崩潰或者加載垃圾數(shù)據(jù)。它們只是指向一個處于僵尸狀態(tài)的對象纯赎。
  7. 當一個弱引用被加載時谦疾,運行時會檢查目標對象的狀態(tài)。如果目標對象是一個僵尸對象犬金,然后它會對弱引用進行歸零餐蔬,也就是減少弱引用計數(shù)并返回 nil
  8. 當僵尸對象的所有弱引用都被歸零佑附,那么這個僵尸對象就會被釋放樊诺。

比起 Objective-C 中的實現(xiàn),這種設計會帶來一些有趣的結(jié)果:

  • 不需要維護一個弱引用列表音同。這樣既簡化代碼也提高了性能词爬。
  • 在一個線程歸零一個弱引用和另外一個線程加載一個弱引用之間就不會存在競態(tài)條件了。這也意味著加載一個弱引用和銷毀一個弱引用對象不需要加鎖权均。這也提高了性能顿膨。
  • 一個對象即便沒有了強引用锅锨,但是弱引用任然會導致該對象被分配的內(nèi)存被占用,直到所有弱引用被加載或者被丟棄恋沃。這種做法臨時增加了內(nèi)存使用必搞。但是要注意的是這個影響很小,當目標對象沒有被釋放時囊咏,它所占的內(nèi)存大小只是實例本身恕洲。當最后一個強引用變成零時,所有的外部資源(包括用于存儲的 ArrayDictionary 屬性)都會被釋放梅割。弱引用會導致被分配的單個實例不會被釋放霜第,而不是整個對象樹。
  • 每一個對象都需要額外的內(nèi)存來存儲弱引用計數(shù)户辞。但在實際的 64 位系統(tǒng)中泌类,這似乎是無關緊要的。頭字段要占據(jù)所有指針大小的塊的數(shù)量底燎,并且強和弱引用計數(shù)共享一個頭字段刃榨。如果沒有弱引用計數(shù),強引用計數(shù)就會占據(jù)整個 64 位双仍。通過使用非指針(non-pointer) isa 可以將強引用移到 isa 中枢希,但我不確定那是不是很重要或者它未來會如何發(fā)展。 對于 32 位系統(tǒng)殊校,弱引用計數(shù)會將對象的大小增加四個字節(jié)晴玖。然而,32 位系統(tǒng)如今已經(jīng)沒有那么重要了.
  • 因為訪問一個弱指針是如此的方便为流,所以 unowned 的語義也采用了相同的機制來實現(xiàn)呕屎。unownedweak 工作方式是一樣的,只是當目標對象被釋放敬察,unowned 會給你一個大大的失敗秀睛,而不是給你返回一個 nil 。在 Objective-C 中莲祸,__unsafe_unretained 是作為一個帶有未定義行為的原始指針來實現(xiàn)的蹂安,你可以快速的訪問它,畢竟加載一個弱指針還是有點慢锐帜。

總結(jié)

Swift 的弱指針通過一種有趣的方式田盈,既保證了速度和正確性,也保證較低的內(nèi)存開銷缴阎。通過追蹤每個對象的弱引用計數(shù)允瞧,將對象的銷毀和對象的析構(gòu)過程分離開來,弱引用問題被安全而又快速的得到解決。正是由于可以查看標準庫的源代碼述暂,這讓我們可以在源代碼級別看到究竟發(fā)生了什么痹升,而不是像我們之前通過反編譯和 dump 內(nèi)存來進行研究。當然畦韭,正如你上面看到的那樣疼蛾,我們很難完全打破這個習慣。

今天就這樣了艺配。下次回來會帶來更多的干貨察郁。由于假期的緣故,可能需要幾周妒挎,但是我會在之前發(fā)布一篇稍微短一點的文章绳锅。不管怎樣西饵,給接下來的話題提更多的建議吧酝掩。周五問答是由讀者們的想法驅(qū)動的,如果你有一個你希望了解的想法眷柔,請告知我!

本文由 SwiftGG 翻譯組翻譯期虾,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg驯嘱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末镶苞,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鞠评,更是在濱河造成了極大的恐慌茂蚓,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剃幌,死亡現(xiàn)場離奇詭異聋涨,居然都是意外死亡,警方通過查閱死者的電腦和手機负乡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門牍白,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抖棘,你說我怎么就攤上這事茂腥。” “怎么了切省?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵最岗,是天一觀的道長。 經(jīng)常有香客問我朝捆,道長般渡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮诊杆,結(jié)果婚禮上歼捐,老公的妹妹穿的比我還像新娘。我一直安慰自己晨汹,他們只是感情好豹储,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著淘这,像睡著了一般剥扣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铝穷,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天钠怯,我揣著相機與錄音,去河邊找鬼曙聂。 笑死晦炊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的宁脊。 我是一名探鬼主播断国,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼榆苞!你這毒婦竟也來了稳衬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤坐漏,失蹤者是張志新(化名)和其女友劉穎薄疚,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赊琳,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡街夭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了慨畸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莱坎。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寸士,靈堂內(nèi)的尸體忽然破棺而出檐什,到底是詐尸還是另有隱情,我是刑警寧澤弱卡,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布乃正,位于F島的核電站,受9級特大地震影響婶博,放射性物質(zhì)發(fā)生泄漏瓮具。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望名党。 院中可真熱鬧叹阔,春花似錦、人聲如沸传睹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欧啤。三九已至睛藻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間邢隧,已是汗流浹背店印。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留倒慧,地道東北人按摘。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像迫靖,于是被迫代替她去往敵國和親院峡。 傳聞我的和親對象是個殘疾皇子兴使,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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