Swift 4 弱引用實現

2021-02-23 知識小集

作者:Mike Ash
譯者:Swift GG 團隊:BigNerdCoding
校對:Yousanflics,numbbbbb,Cee
定稿:CMB

Swift 開源不久我就寫了篇關於弱引用實現的文章。時移勢易,Swift 4 中的弱引用實現已經與舊文不一致了。應 Guillaume Lessard 建議,今天我將會介紹新版實現,並比較其與老版的區別。

舊實現

考慮到有些人可能已經忘記了舊實現並且不願重看前面的文章,下面我們就一起簡要的回顧下之前的實現方式。

在舊實現中,Swift 對象有兩個引用計數:強引用計數和弱引用計數。當強引用計數為 0 而弱引用計數不為 0 時,對象會被銷毀,但是內存並不會被立即釋放。內存中會保留弱引用指向的殭屍對象。

在加載弱引用時,運行時會對引用對象進行檢查。如果是殭屍對象,則會對弱引用計數進行遞減操作。一旦弱引用計數為 0,對象內存將會被釋放。換句話說,殭屍對象的所有弱引用被加載訪問後殭屍對象才會真正被清空。

雖然我喜歡該實現的簡單性,但它有一些缺陷。其中一個就是,殭屍對象可能會長時間停留在內存中。對於那些擁有很多實例的類(因為它們包含許多屬性,或使用類似 ManagedBuffer 分配了內聯的額外內存),這會造成嚴重的內存浪費。

另外,在寫完舊文後我還發現:對於並發讀取,該實現是非線程安全的。雖然已經有補丁修復了這個問題,但從相關討論可以看出,開發者希望找到一個更好的實現方式,避免出現類似問題。

對象數據

Swift 中的 「對象」 其實是由一組數據構成。

首先,最容易想到的就是源碼中聲明的那些可直接訪問的存儲屬性。

其次就是對象的類信息。該信息主要被用於動態派發和 type(of: ) 內置函數。雖然動態派發和 type(of: ) 內置函數從側面暗示了它的存在,但是實際上該信息大多是被隱藏的。

第三種就是各種引用計數信息。除非你進行一些非常規操作,例如,讀取對象的原始內存或說服編譯器讓你調用 CFGetRetainCount,否則這些信息對你來說是完全透明不可見的。

第四種就是 Objective-C 運行時存儲的輔助信息,例如 Objective-C 弱引用列表(Objective-C 的弱引用實現是通過單獨追蹤每個弱引用)和關聯對象。

那麼這些信息最終都存儲在哪裡呢?

在 Objective-C 中,類信息和存儲屬性(例如,實例變量)內聯在對象內存中。其中類信息位於指針所在第一塊內存,其後才是實例變量。輔助類信息則保存在外部表中。當你需要操作關聯對象時,運行時機制會使用內存地址去一個大的哈希表中查找它。為了實現多線程安全,該表在操作時會加鎖,所以存在一定程度訪問速度問題。引用計數的保存位置,則取決於具體作業系統版本和 CPU 架構,它有時位於對象內存中,而有時又存儲在外部表中。

在 Swift 舊有實現中,類信息,引用計數和存儲屬性全部內聯在對象內存中。而輔助信息則依舊存儲在單獨的外部表中。

下面我們不妨將具體實現代碼先放一邊,仔細思考下:理論上應該如何存儲這些信息呢?

每種存儲方案都有利弊。將數據存儲在對象內存中雖然能提高訪問速度,但是會讓內存空間變得吃緊。與之相對,外部存儲方案則是通過犧牲速度來換空間。

Objective-C 傳統存儲方案不將對象引用計數保存在內存中,部分原因正是基於此。因為在 Objective-C 引入引用計數概念時,設備的性能遠不如現在,而且內存容量也極為有限。Objective-C 程序中大多數對象只有一個所有者,即引用計數為 1 。此時在對象內存中騰出 4 個字節空間存儲該引用計數 1 是很浪費的。而外部表方案中,數值 1 可以通過預設默認值方式表示從而減少內存消耗。

每次進行動態方法派發時都需要對象的類信息,所有作為最常用信息,類信息應該直接保存在內存中,存在外部表中是不合適的。

而實例變量這類存儲屬性在編譯期就確定了,而且有現實的訪問速度需求,所以存在對象內存中也是最合理的設計。另外,當對象沒有存儲屬性時,系統不會為其分配內存空間也就不存在浪費問題。

每個對象都需要保留引用計數。雖然不是每個對象的引用計數都為 1,但它依舊是一個相對常見的情形,加上現在內存足夠,它可以直接保存在內存中。

大多數對象都不會有弱引用或關聯對象數據,所有它們應該保存在外部以期節約內存空間。

對於那些有弱引用或關聯對象數據的對象來說,訪問速度確實不夠快但這是合理的權衡結果。那麼問題來了,該舊實現有沒有改進空間和可行方法呢?

Side Tables

在 Swift 弱引用的新版實現代碼中,引入了 side tables 概念來改進上訴缺陷。

Side table 本質就是用於保存額外信息的單獨內存塊,並且它還是可選的。也就是說,對於那些無需保存額外信息的對象來說並沒有多餘開銷。

每個對象都有一個指向其對應 side table 的指針,而 side table 也有一個指針指向該對象。另外,side table 可以存儲關聯的對象數據等其他信息。

為了避免 side table 帶來的 8 字節空間開銷,Swift 做了一個漂亮的優化。通常內存中的第一個字(Word)是類信息,第二個字則是引用計數。當對象存在 side table 需求時,第二個字將保存指向 side table 的指針。因為引用計數是必要信息,所以此時會將引用計數保存到 side table 中。至於程序運行時到底是哪種情形,則由該塊內存中的一個標誌位進行區分。

通過將弱引用從指向對象本身改為指向 side table ,Swift 得以在保留原有引用計數設計的同時修復了舊設計中的缺陷。

因為 side table 比較小並且弱引用不再指向對象本身,這樣之前大型殭屍對象的內存空間將能立即釋放從而降低了內存浪費。同時該實現也讓線程安全問題變得更易解決:不再需要提前將弱引用置空。因為 side table 比較小,指向它的弱引用可以持續保留,直到這些引用自身被覆寫或銷毀。

這裡需要提醒一下,當前 side table 實現中只保存引用計數和指向原始對象的指針。類似保存關聯對象等用途只是一個猜想和假設。因為 Swift 還沒有內建關聯對象功能,而 Objective-C API 仍在使用全局表。

該技術還有不少潛力可挖,也許在不久的將來能看到其應用在關聯對象等內容上。我希望它能為類拓展中的存儲屬性和其他有趣的功能打開一扇新窗。

代碼

因為 Swift 已經開源,所有相關代碼都能直接訪問。

關於 side table 的大部分代碼都在 stdlib/public/SwiftShims/RefCount.h。

高層級的弱引用 API 以及相關注釋都在 swift/stdlib/public/runtime/WeakReference.h。

更多關於堆對象的實現和注釋在 stdlib/public/runtime/HeapObject.cpp。

上述連結其實帶著版本信息,以便後面的讀者也能找到本文內容當時的上下文。如果你想看最新的實現代碼,你在點擊連結後切換到 master 分支即可。

總結

弱引用是一個重要的語言特性。Swift 最初的實現方式非常聰明,也有一些不錯的特性,但是同時也存在一些問題。通過引入 side table,Swift 開發工程師在保留原有特點的同時還解決了這些缺陷。Side table 的實現也為將來更多新特性創造了更多可能性。

今天內容到此為止。下次我還會帶來與編程和代碼相關的新內容。當然你也可以將你感興趣的話題發送給我。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。

推薦閱讀

Swift 4.2 新特性預覽

相關焦點

  • Swift高階之內存管理
    自動引用計數(ARC)屬於Swift的所有權系統,它規定了一組用於管理和轉讓所有權的約定。可以指向對象的變量別名叫做引用。Swift引用具有兩個強度級別:強和弱。此外,弱引用包含無主引用和弱引用。Swift內存管理的本質是:如果一個對象被強引用指向,Swift會保留它,否則將其釋放。剩下的只是實現細節。
  • 從Java到Swift
    4.函數可以嵌套,這個是Java或者C++都沒有的,挺好用。例如經常有一段邏輯,用一個函數實現太長,在Java或者C++中,通常是會把它拆分成幾個函數,保持每個函數短小,功能單一。但是這樣拆分的函數並不能很好的表明他們是一個功能的,不夠「內聚」。用這種Swift函數嵌套的方式就能較好實現。
  • 面試官:說說強引用、軟引用、弱引用、虛引用吧
    弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。弱引用需要用java.lang.ref.WeakReference類來實現,它比軟引用的生存期更短。
  • 理解 Java 中的弱引用
    但是強引用如此之強在一個程序裡,將一個類設置成不可被擴展是有點不太常見的,當然這個完全可以通過類標記成final實現。或者也可以更加複雜一些,就是通過內部包 含了未知數量具體實現的工廠方法返回一個接口(Interface)。舉個例子,我們想要使用一個叫做Widget的類,但是這個類不能被繼承,所以無法 增加新的功能。
  • (原創)手把手,帶你學習實例的交叉引用和弱引用(weak)
    6.9 實例的交叉引用和弱引用(weak) [Swift原創教程]1. 本節課將通過一個實例,演示內存的洩露問題以及如何修復。4.添加第二個屬性,該屬性的類型是一個自定義的類。我們將在後面的代碼中實現該類。
  • iOS面試題-Swift篇
    泛型主要是為增加代碼的靈活性而生的,它可以是對應的代碼滿足任意類型的的變量或方法;泛型可以將類型參數化,提高代碼復用率,減少代碼量// 實現一個方法,可以交換實現任意類型func swap<T>(a: inout T, b: inout T) { (a, b) = (b, a)}訪問控制關鍵字 open
  • SAP ABAP和Java裡的弱引用(WeakReference)和軟引用(SoftReference)
    這就意味著,在ABAP垃圾回收器開始工作的時候,如果一個對象實例並未有任何強引用指向它,此時無論有無弱引用指向它,該對象實例都無法逃脫被回收的命運。看個具體的例子。這個30行的ABAP報表,實現了一個簡單的LCL_PERSON類。第17行創建了一個該類的實例,該實例的強引用存儲在引用變量lo_person裡。
  • Java四種引用類型:強引用、軟引用、弱引用、虛引用
    :強引用、軟引用、弱引用、虛引用。弱引用弱引用的使用和軟引用類似,只是關鍵字變成了WeakReference:WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024\*1024\*10]);System.out.println(weakReference.get());
  • 來一次有側重點的區分Swift與Objective-C
    、面向對象編程,OC注重面向對象編程- swift注重值類型,OC注重指針和引用- swift是靜態類型語言,OC是動態類型語言- swift容易閱讀,文件結構和大部分語法簡易化,只有.swift文件,結尾不需要分號- swift中的可選類型,是用於所有數據類型,而不僅僅局限於類。
  • Swift 語言學習及速查手冊
    代碼: https://github.com/coderzh/CodeTips/blob/master/swift.swift/* swift.swift: Swift 速學速查速用代碼手冊 Source: github.com/coderzh/CodeTips/blob/master/swift.swift Author: coderzh(github.com/coderzh
  • Swift與OC真正去理解Block解決循環引用的技巧
    "%@",weakshop.string);    };    shop.myBlock();Block外部聲明了一個弱引用,在內部使用就不會造成循環引用,所以如果block代碼塊的內部,使用了外面聲明的的弱引用weakshop對象(也就是shop.myBlock代碼塊內部使用了NSLog(@」%@」,weakshop.string);),block代碼塊的內部會自動產生一個弱引用
  • 騰訊Bugly乾貨分享:淺談Swift在實際項目中的應用
    一些常用類型在swift中都有對應,可以相互轉換,通常情況建議使用swift的類型。Object-C中有些機制比如 KVO等在swift中不支持,如果要使用,需要帶上@objc、dynamic等標記。此外,Object-C中一個很方便的代碼互斥方式@synchronized在swift中不支持,因此要實現代碼互斥需要自己使用鎖。
  • 簡單介紹 Swift on Fedora ——在 Fedora 中使用 Swift
    支持方法,擴展和協議的結構函數式編程模式,例如映射和過濾(map and filter)內置強大的錯誤處理功能使用 do,guard,defer 和 repeat 關鍵字編寫高級控制流程在 Fedora 中試用 Swift現已支持在 Fedora 28 中使用 Swift,不過需要安裝名為 swift-lang
  • Swift 正式進入 Windows 平臺
    作者 | Saleem Abdulrasool 來源 | swift.org/blog,點擊閱讀原文查看作者更多文章
  • Java中的 "弱" 引用有啥用?
    當GC運行的時候,發現沒有任何引用指向obj,那麼就會回收obj對象的堆內存空間。換句話說,一個對象被回收, 必須滿足兩個條件:(1)沒有任何引用指向它(2)GC被運行。對於簡單的情況, 手動置空是不需要程式設計師來做的, 因為在java中, 對於簡單對象, 當調用它的方法執行完畢後, 指向它的引用會被從棧中彈出, 所以它就能在下一次GC執行時被回收了。但是, 也有特殊例外.
  • swift語言是什麼?蘋果最新編程swift語言資料
    swift語言是什麼?蘋果最新編程swift語言資料 2014-06-05 11:28 | 作者:SORA | 來源:265G QQ群號:624022706 |
  • 通過LLVM 在 Android 上運行 Swift 代碼
    假設是可以的,我們來看看如何實現。手動構建 Swift 代碼如果使用 Xcode,系統會自動完成這些。我們現在需要手動編譯和連接一個簡單的 Swift "Hello world" :// hello.swiftprint("Hello, world!")
  • 深度分析:前端中的後端-實現篇
    如何寫包含 unit test,formatter,linter 的嚴肅的 swift 代碼(嗯,我之前為了學語言寫過 playground 代碼和 swift UI,但沒有正經寫過包含單元測試的 Swift 代碼)。如何使用 swift protobuf 和在 swift 上做 performance benchmark。
  • Debian中編寫你的第一個Apple Swift程序
    $ sudo apt-get install libcurl4 libpython2.7 libpython2.7-dev 圖4. 安裝必備部分現在你可以安裝Swift了。安裝Swift我們決定在Debian上安裝Swift版本5.0.1。
  • 宣布Swift for TensorFlow已在GitHub上開源
    我們編寫了一些文檔,詳細介紹了我們的理論和實現。這些文檔都可以在 README 文件中找到:https://github.com/tensorflow/swift/blob/master/README.md第一個必讀文檔是「Swift for TensorFlow 設計總覽」,這裡介紹了項目的主要組成部分以及結合方式。