指針
為什么說指針不安全
比如我們在創(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)行泛型指針訪問的過程中,我們并不是使用 loaad
和 store
方法來進(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)換),但是在此之前我們需要先使用 UnsafeRawPointer
將 tuplePtr
轉(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
跟一個代表科目的類 CXSubject
,CXTeacher
中包含科目 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;
}
nativeInit
中 object->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
)。這里 InlineRefCounts
與 HeapObjectSideTableEntry
都是共用一個模板類 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í)例對象 object
跟 refCounts
,只是這里 refCounts
是 SideTableRefCounts
類,所以繼續(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é)論 SideTableRefCountBits
與 InlineRefCountBits
一樣, 都是共用一個模板類 RefCountBitsT
,所以 SideTableRefCountBits
會繼承 RefCountBitsT
的 64 位信息,而且出了 64 位信息之外 SideTableRefCountBits
又多了 32 位的 weakBits
。
無主引用
無主引用與弱引用最大的區(qū)別就是 unowned
修飾的變量 t
假定是永遠(yuǎn)有值的,不是可選類型,所以相對于 weak
,unowned
不夠那么安全, 因?yàn)槿绻?t
變?yōu)榱?nil
程序肯定就會崩潰了。 對于 weak
與 unowned
使用區(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í)行閉包,并對 t
的 age
屬性進(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;
});
例如以上代碼,在 OC
的 block
中我們會用 __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")
}