Swift 指針&內(nèi)存管理

指針

為什么說指針不安全

  • 比如我們在創(chuàng)建一個對象的時候,是需要在堆分配內(nèi)存空間的。但是這個內(nèi)存空間的聲明周期是有限的,也就意味著如果我們使用指針指向這塊內(nèi)容空間,如果當(dāng)前內(nèi)存空間的生命周期到了(引用計(jì)數(shù)為0),系統(tǒng)就會對當(dāng)前的內(nèi)存空間進(jìn)行回收,但是指針的指向并沒有修改,那么這種指針就是野指針,所以當(dāng)前的指針就變成了未定義的行為 。

  • 我們創(chuàng)建的內(nèi)存空間是有邊界的,比如我們創(chuàng)建一個大小為 10 的數(shù)組,這個時候我們通過指針訪問到了 index=11 的位置,這個時候是不是就越界了 訪問了一個未知的內(nèi)存空間。

  • 指針類型與內(nèi)存的值類型不一致,也是不安全的,例如內(nèi)存里面存儲的是 Int 類型,但是指針是 Int8 類型的,這種情況下就會照成精度的缺失。

指針類型

Swift 中的指針分為兩類,typedpointer 指定數(shù)據(jù)類型指針,raw pointer 未指定數(shù)據(jù)類型的指
針(原生指針)。基本上我們接觸到的指針類型有以下幾種:

原始指針的使用

我們一起來看一下如何使用 RawPointer 來存儲 4 個整形的數(shù)據(jù),這里我們需要選取的是 UnsafeMutableRawPointer

struct CXPerson {
    var age: Int = 18
}

//結(jié)構(gòu)體的真實(shí)大小
print(MemoryLayout<CXPerson>.size)
//結(jié)構(gòu)體的步長,存儲一個結(jié)構(gòu)體實(shí)例要跨越的真實(shí)內(nèi)存長度,結(jié)構(gòu)體遵循 8 字節(jié)對齊
print(MemoryLayout<CXPerson>.stride)
//結(jié)構(gòu)體對齊的字節(jié)數(shù)
print(MemoryLayout<CXPerson>.alignment)

泛型指針的使用

這里的泛型指針相比較原生指針來說,其實(shí)就是指定當(dāng)前指針已經(jīng)綁定到了具體的類型。同樣的,我們還是通過一個例子來解釋一下。

在進(jìn)行泛型指針訪問的過程中,我們并不是使用 loaadstore 方法來進(jìn)行存儲操作。這里我們使用到當(dāng)前泛型指針內(nèi)置的變量 pointee。獲取 UnsafePointer 的方式有兩種,下面我們來分別介紹一下。

通過已有變量獲取

如上圖所示,我們可以通過獲取已有變量來修改變量 age 的值。只是方式 1 跟 方式 2 是獲取到的指針跟指針指向的內(nèi)容都是不可變的,所以沒法直接修改 ptr.pointee,但是方式 3 獲取到的指針跟指針指向的內(nèi)容都是可變的,所以可以直接修改 ptr.pointee

直接分配內(nèi)存

        // 開辟一塊可以連續(xù)存儲 5 個 CXPerson 類型大小的內(nèi)存,并返回地址 tptr
        let tptr = UnsafeMutablePointer<CXPerson>.allocate(capacity: 5)
        
        //tptr 為起始地址,可以通過下標(biāo)的方式存儲 CXPerson 實(shí)例
        tptr[0] = CXPerson(age: 10, height: 110)
        tptr[1] = CXPerson(age: 10, height: 110)
        
        //也可以通過 initialize 存儲
        tptr.advanced(by: 2 * MemoryLayout<CXPerson>.stride).initialize(to: CXPerson(age: 10, height: 110))
        
        // deinitialize 與 deallocate 是成對出現(xiàn)的,dein itialize 可以理解為將數(shù)據(jù)清 0
        tptr.deinitialize(count: 5)
        // 回收這塊內(nèi)存空間
        tptr.deallocate()

內(nèi)存綁定

swift 提供了三種不同的 API 來綁定/重綁定指針:

  • assumingMemoryBound(to:)

這里我們定義了一個 testPoint 函數(shù),需要傳入一個 UnsafePointer<Int> 類型的指針參數(shù),當(dāng)我們調(diào)用 testPoint 函數(shù)并直接傳入 tuplePtr 會報(bào)錯,這里元組是值類型,本質(zhì)上這塊內(nèi)存空間中存放的就是 Int 類型的數(shù)據(jù),這時候 UnsafePointer<Int>UnsafePointer<(Int, Int)> 本質(zhì)上是一樣的,都是指向 tuple 的首地址,所以這個時候我們就可以使用 assumingMemoryBound(to:) 進(jìn)行轉(zhuǎn)換。

在我們處理代碼的過程中,如果只有原始指針(沒有保留指針類型),但是我們明確知道指針類型的情況下,我們就可以使用 assumingMemoryBound(to:) 來告訴編譯器預(yù)期的類型(注意:這里只是讓編譯器繞過類型檢查,并沒有發(fā)生實(shí)際的類型轉(zhuǎn)換),但是在此之前我們需要先使用 UnsafeRawPointertuplePtr 轉(zhuǎn)換成原生指針,這個時候就不會報(bào)錯了。

  • bindMemory(to:capacity:)

使用 bindMemory(to:capacity:) 可以用于更改內(nèi)存綁定的類型,如果當(dāng)前內(nèi)存還沒有類型綁定,則將首次綁定為該類型;否則重新綁定該類型,并且內(nèi)存中所有的值都會變成該類型。

  • withMemoryRebound(to:capacity:)
func testPoint(_ p: UnsafePointer<Int8>) {
    print(p)
}

let Uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)

Uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1){ (int8Ptr: UnsafePointer<Int8>) in
    testPoint(int8Ptr)
}

當(dāng)我們在給外部函數(shù)傳遞參數(shù)時,不免會有一些數(shù)據(jù)類型上的差距。如果我們進(jìn)行類型轉(zhuǎn)換,必然要來回復(fù)制數(shù)據(jù),這個時候我們可以使用 withMemoryRebound(to:capacity:) 來臨時更改內(nèi)存綁定類型,可以減少代碼的復(fù)雜度。

內(nèi)存管理

swift 中也是一樣,是使用自動引用計(jì)數(shù)(ARC)機(jī)制來追蹤和管理內(nèi)存。

在對象的內(nèi)存布局中,其中的 8 個字節(jié)是用來存儲當(dāng)前的引用計(jì)數(shù)的,但是我們通過打印并不能看出具體的引用計(jì)數(shù),所以下面我們通過源碼來看一下。

首先我們先找到 refCount 的定義,這里我們在 HeapObject.h 文件中搜索:

接著我們追蹤 InlineRefCounts


這里可以看到 RefCounts 是一個模板類,接收一個泛型參數(shù) InlineRefCountBits ,本質(zhì)上 RefCounts 在操作 API 的時候其實(shí)操作的都是傳進(jìn)來的泛型參數(shù) RefCountBits,所以 RefCounts 本質(zhì)上是對引用計(jì)數(shù)的包裝,而引用計(jì)數(shù)的具體類型取決于傳入的泛型參數(shù) InlineRefCountBits 。所以我們追蹤 InlineRefCountBits

這里可以看到 InlineRefCountBits 是一個模板函數(shù) RefCountBitsT,傳遞的參數(shù) RefCountIsInline 要么是 true 要么是 false, 所以我們引用計(jì)數(shù)真實(shí)操作的類就是 RefCountBitsT,所以我們追蹤 RefCountBitsT,看一下 RefCountBitsT 中有什么屬性信息。


這里我們可以看到就一個屬性信息 bits,但是 bits 是由 RefCountBitsInt 中的 Type 屬性來定義的,所以我們追蹤 RefCountBitsInt 可以看到 Type 是一個 uint64_t 的位域信息,所以到這里我們可以看到,swift 中引用計(jì)數(shù)跟 oc 中引用計(jì)數(shù)都是一個 64 位的位域信息,在里面存儲了跟生命周期相關(guān)的引用計(jì)數(shù)。

當(dāng)我們創(chuàng)建一個實(shí)例對象的時候,當(dāng)前的引用計(jì)數(shù)是多少呢?這里我們也找到源代碼看一下。



在源代碼中可以看到初始化方法,追蹤初始化方法可以看到初始化賦值,然后追蹤 Initialized 的定義,可以看到 Initialized_t 是一個枚舉類型, 依據(jù)枚舉類型可以找到 RefCountBits(0, 1),這里 RefCountBits(0, 1) 就是我們上面講的 RefCountBitsT,所以我們找到 RefCountBitsT 類,來看一下它的初始化方法:

通過上面的源碼追蹤我們知道 strongExtraCount 為 0,unownedCount 為 1,在初始化方法中 strongExtraCount 左移了 StrongExtraRefCountShift 位, unownedCount 左移了 UnownedRefCountShift 位。所以在這個過程當(dāng)中其實(shí)就是把強(qiáng)引用計(jì)數(shù)跟無主引用計(jì)數(shù)通過位移的方式存儲到了 64 位的信息當(dāng)中。這里 StrongExtraRefCountShift 是 33,UnownedRefCountShift 是 1。

下面我們對代碼進(jìn)行一些修改,來看一下強(qiáng)引用計(jì)數(shù)的值:

這里可以看到把 p 賦值給 p1 之后強(qiáng)引用計(jì)數(shù)為 1,賦值給 p2 之后強(qiáng)引用計(jì)數(shù)為 2。這里大家可能會有疑問,強(qiáng)引用是怎么添加的,這里一樣,我們也通過源碼來看一下。



通過代碼追蹤,我們可以看到強(qiáng)引用的時候函數(shù)調(diào)用流程是 _swift_retain_ -> increment -> incrementStrongExtraRefCount,在 incrementStrongExtraRefCount 函數(shù)中會對原有的引用計(jì)數(shù)進(jìn)行左移 33 位的操作。

循環(huán)引用

以上我們了解了強(qiáng)引用的底層實(shí)現(xiàn),但是使用強(qiáng)引用的時候也會遇到循環(huán)引用的問題,這里我們來看一個經(jīng)典的循環(huán)引用的案例:

class CXTeacher {
    var age: Int = 18
    var name: String = "chenxi"
    var subject: CXSubject?
}

class CXSubject {
    var subjectName: String
    var subjectTeacher: CXTeacher
    
    init(_ subjectName: String, _ subjectTeacher: CXTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

var t = CXTeacher()
var subject = CXSubject("swift", t)
t.subject = subject

這里我們定義了一個老師的類 CXTeacher 跟一個代表科目的類 CXSubjectCXTeacher 中包含科目 subject,而 CXSubject 中又包含 subjectTeacher,這樣就造成了循環(huán)引用。Swift中提供了兩種方式來解決這個問題:弱引用(weak reference) 和無主引用(unowned reference)。

弱引用

弱引用不會對其引用的實(shí)例保持強(qiáng)引用,因而不會阻上 ARC 釋放被引用的實(shí)例。這個特性阻止了引用變?yōu)檠h(huán)強(qiáng)引用。聲明屬性或者變量時,在前面加上 weak 關(guān)鍵字表明這是一個弱引用。

由于弱引用不會強(qiáng)保持對實(shí)例的引用,所以說實(shí)例被采釋放了弱引用仍舊引用著這個實(shí)例也是有可能的。因此,ARC 會在被引用的實(shí)例被釋放是自動地設(shè)置弱引用為 nil。由于弱引用需要允許它們的值為 nil,它們一定得是可選類型。


通過以上代碼我們可以看到,weak 修飾的 t 是一個可選類型,而且通過匯編調(diào)試的時候可以看到會調(diào)用 swift_weakInit 函數(shù),下面我們到源碼中具體來看一下。

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
 ref->nativeInit(value);
 return ref;
}

聲明一個 weak 變量相當(dāng)于定義了一個 WeakReference 對象。

 void nativeInit(HeapObject *object) {
   auto side = object ? object->refCounts.formWeakReference() : nullptr;
   nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
 }
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}

nativeInitobject->refCounts 調(diào)用了 formWeakReference,而在 formWeakReference 中其實(shí)就是創(chuàng)建了一個散列表,所以我們繼續(xù)追蹤以下散列表的創(chuàng)建方法 allocateSideTable

HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
// 取出原有的 refCounts 
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // 判斷 refCounts 是否有引用計(jì)數(shù)
  if (oldbits.hasSideTable()) {
    // 如果有就獲取引用計(jì)數(shù)
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // 如果沒有或者正在析構(gòu)直接返回 nullptr 
    return nullptr;
  }
 
// 如果沒有 SideTable這里需要創(chuàng)建一個新的 HeapObjectSideTableEntry 類型的 side 
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());

// 將 side 傳入 InlineRefCountBits 中來做初始化操作
auto newbits = InlineRefCountBits(side);

do {
    if (oldbits.hasSideTable()) {
      // Already have a side table. Return it and delete ours.
      // Read before delete to streamline barriers.
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      // Already past the start of deinit. Do nothing.
      return nullptr;
    }
    
    side->initRefCounts(oldbits);
    
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}

這里我們繼續(xù)追蹤 HeapObjectSideTableEntry

通過源碼注釋可以看到 swift 中存在兩種引用計(jì)數(shù), 如果只有強(qiáng)引用就是 InlineRefCounts,包含了 strong RC + unowned RC + flags,如果存在弱引用就是 HeapObjectSideTableEntry,包含了 strong RC + unowned RC + weak RC + flags,就是原先的 64 為信息(strong RC + unowned RC + flags)上加上一個 32 位的弱引用 信息(weak RC)。這里 InlineRefCountsHeapObjectSideTableEntry 都是共用一個模板類 RefCountBitsT

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

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

追蹤 HeapObjectSideTableEntry 類可以看到 HeapObjectSideTableEntry 中存儲了實(shí)例對象 objectrefCounts,只是這里 refCountsSideTableRefCounts 類,所以繼續(xù)追蹤 SideTableRefCounts

typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts; 
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits;

  public:
    SWIFT_ALWAYS_INLINE
    SideTableRefCountBits() = default;

    SWIFT_ALWAYS_INLINE
    constexpr SideTableRefCountBits(uint32_t strongExtraCount,
                                    uint32_t unownedCount)
        : RefCountBitsT<RefCountNotInline>(strongExtraCount, unownedCount)
          // weak refcount starts at 1 on behalf of the unowned count
          ,
          weakBits(1) {}

這里就驗(yàn)證了我們上面的結(jié)論 SideTableRefCountBitsInlineRefCountBits 一樣, 都是共用一個模板類 RefCountBitsT,所以 SideTableRefCountBits 會繼承 RefCountBitsT 的 64 位信息,而且出了 64 位信息之外 SideTableRefCountBits 又多了 32 位的 weakBits

無主引用

無主引用與弱引用最大的區(qū)別就是 unowned 修飾的變量 t 假定是永遠(yuǎn)有值的,不是可選類型,所以相對于 weakunowned 不夠那么安全, 因?yàn)槿绻?t 變?yōu)榱?nil 程序肯定就會崩潰了。 對于 weakunowned 使用區(qū)別總結(jié)如下。

  • weak:如果需要互相引用的實(shí)例,生命周期沒有任何關(guān)聯(lián)可以使用 weak,例如 delegate

  • unowned:如果能確定互相引用的實(shí)例,其中一個實(shí)例銷毀另一個實(shí)例也跟著銷毀可以使用 unowned,例如我們上面講的老師跟課程的例子就可以使用 unowned。從性能上來講 unowned 性能能好,因?yàn)椴挥妙~外創(chuàng)建散列表,直接操作 64 位的信息就行。

class CXTeacher {
    var age: Int = 18
    var name: String = "chenxi"
    unowned var subject: CXSubject?
}

class CXSubject {
    var subjectName: String
    var subjectTeacher: CXTeacher

    init(_ subjectName: String, _ subjectTeacher: CXTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

閉包循環(huán)引用

閉包一般默認(rèn)會捕獲外部變量,通過上面我們也可以看出,在閉包中對 age 進(jìn)行加 1,打印的時候 age 變?yōu)榱?19,閉包對變量的修改將會改變外部原始變量的值。

那么這樣的話就會有一個問題,如果我們在 class 的內(nèi)部定義一個閉包,當(dāng)前閉包訪問屬性過程中就會對我們當(dāng)前實(shí)例對象進(jìn)行捕獲。

如上案例中我們在 CXTeacher 類中定義了一個閉包屬性 testClosure,在 testARC 執(zhí)行閉包,并對 tage 屬性進(jìn)行加 1,這里可以看到 deinit 并沒有被調(diào)用,這是因?yàn)?CXTeacher 與 閉包屬性 testClosure 產(chǎn)生了循環(huán)因?yàn)椤_@里我們可以使用捕獲列表來解決這個問題。

這里我們可以看到 deinit 函數(shù)執(zhí)行了,在這里 [unowned t] 就是捕獲列表,那么什么是捕獲列表呢?

捕獲列表

默認(rèn)情況下,閉包表達(dá)式從其周圍的范圍捕獲常量和變量,并強(qiáng)引用這些值。您可以使用捕獲列表來顯式控制如何在閉包中捕獲值。

在參數(shù)列表之前,捕獲列表被寫為用逗號括起來的表達(dá)式列表,并用方括號括起來。如果使用捕獲列表,則即使省略參數(shù)名稱,參數(shù)類型和返回類型,也必須使用 in 關(guān)鍵字。

創(chuàng)建閉包時,將初始化捕獲列表中的條目。對于捕獲死列表中的每個條目,將常量初始化為在周圍范圍內(nèi)具有相同名稱的常量或變量的值。例如,在面上面的代媽中,捕獲列表中包含 age,但捕獲列表中未包含 height,這使它們具有不同的行為。

創(chuàng)建閉包時,內(nèi)部作用域中的age會用外部作用域中的 age 的值進(jìn)行初始化,但它們的值未以任何特殊方式連接。這意味著更改外部作用域中的a的值不會景影響內(nèi)部作用域中的 age 的值,也不會更改封閉內(nèi)部的值,也不會影響封閉外部的值。相比之下,只有一個名為 height 的變量,外部作用域中的 height,因此,在閉包內(nèi)部或外部進(jìn)行的更改在兩個地方均可見。

__weak __typeof__(self) weakSelf = self; 
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
 __strong __typeof(self) strongSelf = weakSelf;  
}); 

例如以上代碼,在 OCblock 中我們會用 __strong __typeof(self) strongSelf = weakSelf 來延長 weakSelf 的生命周期,__strong 確保在 Block 內(nèi),strongSelf 不會被釋放。那么在 swift 中我們可以用如下兩種方式實(shí)現(xiàn)類似功能。

  • 方式 1
func testARC() {
    var t = CXTeacher()

    t.testClosure = { [weak t] in
        if let strongT = t {
            print(strongT.age)
        }
    }
    print("end")
}
  • 方式 2
func testARC() {
    var t = CXTeacher()

    t.testClosure = { [weak t] in
        withExtendedLifetime(t) { //延長t的生命周期在這個閉包表達(dá)式范圍內(nèi)
            print(t!.age)
        }
    }
    print("end")
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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