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

做者:Mike Ash,原文連接,原文日期:2015-12-11
譯者:riven;校對:Cee;定稿:千葉知風html

即使你已經在火星的一個洞穴裏,緊閉着你的雙眼而且捂住你的耳朵,也避免不了 Swift 已經開源的事實。正由於開源,咱們可以更加方便地去探索 Swift 中的不少有趣的特性,其中之一即是 Swift 中弱引用是如何工做的問題。git

弱引用

在採用垃圾回收器或者引用計數進行內存管理的語言中,強引用可使得特定的對象一直存活,但弱引用就沒法保證。當一個對象被強引用時,它是不可以被銷燬的;可是若是它是個弱引用,就能夠。github

當咱們所提到「弱引用」時,一般的意思是指一個歸零弱引用(Zeroing Weak Reference)。也就是說,當弱引用的目標對象被銷燬時,弱引用就會變成 nil(校者注:看這篇文章瞭解更多)。非歸零弱引用也是存在的,它會致使一些陷阱(Trap)、崩潰(Crash)或者未定義行爲的調用。好比你在 Objective-C 中使用 unsafe_unretained,或者在 Swift 中使用 unowned(Objective-C 未定義行爲處理方式,而 Swift 卻很可靠地處理這些崩潰)。objective-c

歸零弱引用很方便使用,在基於引用計數進行內存管理的語言中他們是很是有用的。它們容許循環引用存在卻不會產生死循環,而且不須要手動打破逆向引用。他們很是的有用,在蘋果引入 ARC 和讓弱引用在垃圾收集代碼以外的語言層面上可用以前,我就已經實現了我本身的弱引用版本swift

它是如何工做的呢?

歸零弱引用比較典型的實現方式是保持一個對每一個對象的全部弱引用列表。當對一個對象建立了弱引用,這個引用就會被添加到這個列表中。當這個引用被從新賦值或者超出了其做用域,它就會從列表中被移除。當一個對象被銷燬,這個列表中的全部引用都會被歸零。在多線程的狀況下,其實現必須是同步獲取一個弱引用並銷燬一個對象,以免競態條件的出現:好比當一個線程釋放某個對象的最後一個強引用而同時另外一個線程卻試圖加載一個它的一個弱引用。安全

在個人實現中,每個弱引用都是一個完整的對象。弱引用列表是一個弱引用對象的集合。雖然因爲額外的轉換和內存使用讓效率變低了,但這種方式能夠很方便的讓這些引用變成完整的對象。多線程

蘋果公司的 Objective-C 的實現是這樣的,每個弱引用是一個指向目標對象的普通指針。編譯器並不直接讀寫指針,而是使用一些幫助函數。當存儲一個弱指針時,存儲函數會將指針的位置註冊爲目標對象的一個弱引用。因爲讀取函數被集成進了引用計數系統,這就確保了在讀取一個弱指針時,不會返回一個已經被釋放了的對象的指針。app

歸零操做

讓咱們建立一些代碼來研究一下它們到底是怎麼運行的。函數

咱們但願寫一個函數可以 dump 一個對象的內存內容。這個函數接受一塊內存區域,將其按指針大小進行分塊,而且將最終的結果轉換成一個易於查看的十六進制字符串:性能

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(" ")
}

下一個函數會爲一個對象建立一個 dump 函數。調用時傳入一個對象,它會返回一個 dump 這個對象內容的函數。在函數內部,咱們給對象保存了一個 UnsafePointer,而不是普通的引用。這樣能夠確保它不會和語言的引用計數系統發生交互。它容許咱們能夠在這個對象被銷燬以後 dump 出它的內存,後面咱們會介紹。

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 變量,以便於咱們區分弱引用在 dump 出來的內存結構中的位置:

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

讓咱們試一下! 咱們先建立一個引用,而後 dump 它:

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

打印結果:

WeakReferer 0x00007f8a3861b920: 0000000107ab24a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

咱們看到 isa 指針位於最開始的位置,緊隨其後的是其它一些內部字段。dummy1 變量佔據了第四塊,dummy2 變量佔據了第六塊。正如咱們所指望的那樣,在他們之間的弱引用正好是零。

如今咱們讓這個弱引用指向一個目標對象,看看會變成什麼樣。我將這段代碼放入一個 do語句中,以便於當目標對象超出做用域和被銷燬時咱們能夠進行控制:

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

打印結果:

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

正如咱們指望的那樣,目標對象的指針直接存儲在弱引用中。在目標對象被銷燬以後,咱們在 do 代碼塊以後再次調用 dump 函數:

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

不幸的是它並無被歸零。多是目標對象沒有被銷燬。必定是有某些東西讓它繼續活着!讓咱們再檢查一下:

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

再次運行代碼,結果以下:

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

它消失了,可是弱引用並無歸零。怎麼回事呢,咱們發現了 Swift 的一個 bug!很神奇,這個 bug 一直沒有被解決。你會想以前確定已經有人已經注意到了這個問題。接下來,咱們經過訪問弱引用來產生一個崩潰,而後咱們能夠用這個 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

哦,個人天吶!大爆炸在哪呢?應該有一個驚天動地的大爆炸呀!輸出的內容代表一切工做正常,但咱們能夠清楚地從 dump 內容看到它並無正常工做。

讓咱們再仔細檢查一下。下面是一個通過修改的 WeakTarget 類,咱們添加了一個 dummy 變量以便於區分 dump 的內容:

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())

讓咱們檢查一下輸出內容。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 字段會產生 nil ,即使它沒有被歸零:

nil

再次 dump referer 對象的內容,從中咱們看出僅僅訪問 target 字段的行爲已經改變了它。如今它被歸零了:

WeakReferer 0x00007fe174802520: 000000010faa64a0 0000000200000004 1234321012343210 0000000000000000 abcdefabcdefabcd

目標對象如今被徹底抹掉了:

WeakTarget 0x00007fe17341d270: 200007fe17342a04 300007fe17342811 ffffffffffff0002

如今變的愈來愈有趣了。咱們看到頭字段會一下子增長,一下子減小;讓咱們看看是否能有重現出更多的信息:

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())

打印結果:

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

咱們看到每個新的弱引用會讓頭字段中的第一個數增長 2。每個新的強引用會讓頭字段中的第二個數增長 4。

回顧一下,下面這些就是目前咱們所發現的:

  • 在內存中弱指針和普通指針是同樣的.

  • 當一個弱目標對象(WeakTarget)的 deinit 方法調用時,目標對象是不會被釋放的,而且弱指針也不會被歸零。

  • 當目標對象的 deinit 方法執行以後,訪問弱指針,它就會被歸零而且弱目標對象也會被釋放。

  • 弱目標對象包含一個弱引用的引用計數,與強引用計數分離開。

Swift 代碼

既然 Swift 已經開源,咱們能夠經過查看源代碼來繼續咱們的觀察。

在 Swift 標準庫中用 HeapObject 類型來表示一個分配在堆上的對象,其實現可參考 stdlib/public/SwiftShims/HeapObject.h。看起來是這樣的:

cpp
struct HeapObject {
/// 這始終是一個有效的元數據對象的指針。
struct HeapMetadata const *metadata;

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

#ifdef __cplusplus
HeapObject() = default;

// 給新分配的堆內存初始化空間(對象alloc,是分配的堆內存)。
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

噢,快看!這就是咱們的兩個引用計數。

(附加問題:爲何這裏強引用在前面,而在 dump 時確是弱引用在前面?)

引用計數是經過位於 stdlib/public/runtime/HeapObject.cpp 文件中的一系列函數來進行管理的。好比,下面的 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_;

這裏面拐了幾個彎,但它最終是調用頭文件中的內聯函數:

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

如你所見,它會增長引用計數。下面是 increment 函數的實現:

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
};

相信你已經明白爲何每個新的強引用會讓頭字段增長 4 了吧。這個枚舉類型的前兩位用來做爲標誌位。回想一下以前的 dump 結果,咱們能夠看到這些標誌位。下面是一個弱目標對象在最後一個強引用消失以前和以後的結果:

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

其中第二個字段開始是 4,表示引用計數爲 1 而且沒有標誌位,以後變成了 2,表示引用計數爲 0 和 RC_DEALLOCATING_FLAG 標誌位被設定了。這個被析構的對象被放在了處於 DEALLOCATING 狀態的位置。

(順便說一句,RC_PINNED_FLAG 究竟是用來幹什麼的呢?我查找了相關代碼,除了可以代表一個「固定的對象(pinned object)」外,其它對於這個標記一無所知。若是你弄清楚了或者有一些相關的猜想,請給我留言。)

如今讓咱們看一看弱引用計數的實現。它有一樣的枚舉結構:

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。我猜它們曾經是相等的,但後來它被修改了而註釋卻沒有更新。我只是想代表若是註釋是無用的,那你爲何還要寫它呢。

全部這些是如何和加載弱引用相關聯的呢?它是由 swift_weakLoadStrong 函數來處理的:

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);
}

從上面的代碼,惰性歸零是如何工做的已經一目瞭然了。當加載一個弱引用時,若是目標對象正在被銷燬,就會對這個引用進行歸零。反之,會保留目標對象並返回它。進一步深挖一點,咱們能夠看到 swift_weakRelease 如何釋放對象的內存,前提是它是最後一個引用:

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」命名的地方大多數都改爲了「unowned」。上面的命名是截至撰寫本文時最新的快照,但開發仍在繼續。你能夠查看和我這對應的版本庫中的 2.2 版本的快照,或者獲取最新的版本可是要注意命名的變化,而且實現也有可能發生了改變。)

整合

咱們已經在層級上自上往下地看到了 Swift 中的弱引用是如何實現的。那麼在高層觀察 Swift 的弱引用又是如何工做的呢?

  1. 弱引用只是指向目標對象的指針。

  2. 在 Objective-C 中是沒有辦法單獨追蹤弱引用的。

  3. 相反,每個 Swift 對象都有一個弱引用計數,和它的強引用計數相鄰。

  4. Swift 將對象的析構過程(deinit)和對象的釋放(dealloc)解耦。一個對象能夠被析構並釋放它的外部資源,但沒必要釋放對象自己所佔用的內存。

  5. 當一個 Swift 對象的強引用計數變成零而弱引用計數仍大於零時,那麼這個對象會被析構,可是不會被釋放。

  6. 這意味着一個被釋放對象的弱指針仍然是一個有效的指針,它能夠被反向引用而不會崩潰或者加載垃圾數據。它們只是指向一個處於殭屍狀態的對象。

  7. 當一個弱引用被加載時,運行時會檢查目標對象的狀態。若是目標對象是一個殭屍對象,而後它會對弱引用進行歸零,也就是減小弱引用計數並返回 nil

  8. 當殭屍對象的全部弱引用都被歸零,那麼這個殭屍對象就會被釋放。

比起 Objective-C 中的實現,這種設計會帶來一些有趣的結果:

  • 不須要維護一個弱引用列表。這樣既簡化代碼也提升了性能。

  • 在一個線程歸零一個弱引用和另一個線程加載一個弱引用之間就不會存在競態條件了。這也意味着加載一個弱引用和銷燬一個弱引用對象不須要加鎖。這也提升了性能。

  • 一個對象即使沒有了強引用,可是弱引用任然會致使該對象被分配的內存被佔用,直到全部弱引用被加載或者被丟棄。這種作法臨時增長了內存使用。可是要注意的是這個影響很小,當目標對象沒有被釋放時,它所佔的內存大小隻是實例自己。當最後一個強引用變成零時,全部的外部資源(包括用於存儲的 ArrayDictionary 屬性)都會被釋放。弱引用會致使被分配的單個實例不會被釋放,而不是整個對象樹。

  • 每個對象都須要額外的內存來存儲弱引用計數。但在實際的 64 位系統中,這彷佛是可有可無的。頭字段要佔據全部指針大小的塊的數量,而且強和弱引用計數共享一個頭字段。若是沒有弱引用計數,強引用計數就會佔據整個 64 位。經過使用非指針(non-pointer) isa 能夠將強引用移到 isa 中,但我不肯定那是否是很重要或者它將來會如何發展。 對於 32 位系統,弱引用計數會將對象的大小增長四個字節。然而,32 位系統現在已經沒有那麼重要了.

  • 由於訪問一個弱指針是如此的方便,因此 unowned 的語義也採用了相同的機制來實現。unownedweak 工做方式是同樣的,只是當目標對象被釋放,unowned 會給你一個大大的失敗,而不是給你返回一個 nil 。在 Objective-C 中,__unsafe_unretained 是做爲一個帶有未定義行爲的原始指針來實現的,你能夠快速的訪問它,畢竟加載一個弱指針仍是有點慢。

總結

Swift 的弱指針經過一種有趣的方式,既保證了速度和正確性,也保證較低的內存開銷。經過追蹤每一個對象的弱引用計數,將對象的銷燬和對象的析構過程分離開來,弱引用問題被安全而又快速的獲得解決。正是因爲能夠查看標準庫的源代碼,這讓咱們能夠在源代碼級別看到究竟發生了什麼,而不是像咱們以前經過反編譯和 dump 內存來進行研究。固然,正如你上面看到的那樣,咱們很難徹底打破這個習慣。

今天就這樣了。下次回來會帶來更多的乾貨。因爲假期的緣故,可能須要幾周,可是我會在以前發佈一篇稍微短一點的文章。無論怎樣,給接下來的話題提更多的建議吧。週五問答是由讀者們的想法驅動的,若是你有一個你但願瞭解的想法,請告知我!

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索