Swift 三归露、指針 & 內(nèi)存管理

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

一洲脂、指針

1.1 為什么說指針不安全

  • ?如我們在創(chuàng)建?個對象的時候,是需要在堆分配內(nèi)存空間的剧包。但是這個內(nèi)存空間的生命周期是有限的恐锦,也就意味著如果我們使?指針指向這塊內(nèi)存空間往果,如果當(dāng)前內(nèi)存空間的?命周期已經(jīng)到了(引?計數(shù)為0),那么我們當(dāng)前的指針是不是就變成了未定義的行為了一铅。
  • 我們創(chuàng)建的內(nèi)存空間是有邊界的陕贮,?如我們創(chuàng)建?個??為10的數(shù)組,這個時候我們通過指針訪問 到了 index = 11的位置馅闽,這個時候數(shù)組是不是就越界了飘蚯,訪問了?個未知的內(nèi)存空間。
  • 指針類型與內(nèi)存的值類型不?致福也,也是不安全的。

1.2 指針類型

Swift中的指針分為兩類, typed pointer 指定數(shù)據(jù)類型指針, raw pointer 未指定數(shù)據(jù)類型的指針(原?指針)攀圈”┐眨基本上我們接觸到的指針類型有以下?種:

指針類型.png

1.2.1 原生指針的使用

我們?起來看?下如何使? Raw Pointer 來存儲 4 個整型的數(shù)據(jù),這?我們需要選取的是 UnsafeMutableRawPointer

///1赘来、開辟一塊內(nèi)存空間
/// UnsafeMutableRawPointer存儲原生指針
/// allocate:開辟空間
///byteCount: 當(dāng)前總的內(nèi)存大小 4個Int整型现喳,一個整型是8字節(jié),共32字節(jié)
///alignment: 對齊的大小

///2犬辰、調(diào)用storeBytes方法存儲當(dāng)前的整型數(shù)值
///of: 存儲值
///as: 值的類型嗦篱,當(dāng)前類型是整型

///3、我們打印輸出驗證一下
///調(diào)用load方法加載當(dāng)前內(nèi)存當(dāng)中的數(shù)據(jù)
///fromByteOffset: 距離首地址的字節(jié)的大小幌缝,每次移動i * 8字節(jié)
///as: 值的類型灸促,當(dāng)前類型是整型

///4、收回并釋放對應(yīng)的內(nèi)存空間
/// deallocate

let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)

for i in 0 ..< 4 {
    p.storeBytes(of: i, as: Int.self)
}

for i in 0 ..< 4 {
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index: \(i), value: \(value)")
}
p.deallocate()

編譯運行涵卵,我們調(diào)用打印數(shù)據(jù)查看一下

index: 0, value: 3
index: 1, value: 0
index: 2, value: 16
index: 3, value: 0

結(jié)果發(fā)現(xiàn)浴栽,這個打印結(jié)果并不能和我們存儲的數(shù)據(jù)一致。那么問題出在哪里哪轿偎?
我們在存儲后打印一下p的內(nèi)存地址

省略......
for i in 0 ..< 4 {
    p.storeBytes(of: i, as: Int.self)
}

print(p)

lldb打印結(jié)果如下:

0x000000010073c4e0
(lldb) x/8g 0x000000010073c4e0
0x10073c4e0: 0x0000000000000003 0x0000000000000000
0x10073c4f0: 0x0000000000000010 0x0000000000000000
0x10073c500: 0x000000004d55545a 0x000020a000000000
0x10073c510: 0x4d55545a00000000 0x0000000000000000

這?很顯然是我們對當(dāng)前數(shù)據(jù)的存儲?式不對典鸡,按道理來說我們是 8 個字節(jié), 8個字節(jié)的排列開來坏晦,? 在這個過程中存儲的好像不知道每個數(shù)據(jù)與數(shù)據(jù)之間的間距萝玷,所以這?我們需要指定?個東?,那就是每個數(shù)據(jù)之間在內(nèi)存中的間距昆婿。

經(jīng)探究發(fā)現(xiàn)球碉,是因為我們存儲時storeBytes并沒有移動對應(yīng)的步長信息導(dǎo)致的。那么什么是步長信息哪挖诸,我們首先要了解一個枚舉類型MemoryLayout汁尺。

為了便于理解MemoryLayout,我們先來看下下面這個例子:

struct ZGTeacher {
    var age: Int = 18
}

let size = MemoryLayout<ZGTeacher>.size
let stride = MemoryLayout<ZGTeacher>.stride
let alignment = MemoryLayout<ZGTeacher>.alignment

print(size, stride, alignment)

lldb打印結(jié)果

8 8 8

我們給ZGTeacher結(jié)構(gòu)體新增一個Bool屬性看一下它的變化多律。

struct ZGTeacher {
    var age: Int = 18
///新增一個Bool屬性
    var sex: Bool = true
}
省略......

lldb打印結(jié)果如下:

9 16 8

其中 alignment 是不變的痴突,但是 sizestride 都變了搂蜓。

當(dāng)前結(jié)構(gòu)體的MemoryLayout.png

MemoryLayout是Swift標(biāo)準(zhǔn)庫中定義的一個枚舉,顧名思義其是用于獲取內(nèi)存相關(guān)信息辽装,MemoryLayout<Int>則是一種泛型的用法帮碰,調(diào)用其size屬性可以獲取某種數(shù)據(jù)類型所占內(nèi)存空間的字節(jié)數(shù),是其在內(nèi)存中真實占用大小拾积。調(diào)用其stride屬性可以獲取某種數(shù)據(jù)類型所開辟內(nèi)存空間的字節(jié)數(shù)殉挽,是系統(tǒng)為其分配的內(nèi)存大小。調(diào)用其alignment屬性可以獲取某種數(shù)據(jù)類型所需要的對齊信息拓巧,指的是其當(dāng)前內(nèi)存對齊方式斯碌,是1字節(jié)對齊,4字節(jié)對齊等肛度。

這?我們回到我們的指針操作傻唾,此時我們應(yīng)該明?我們在存儲 4 個連續(xù)整型的數(shù)據(jù)時候的問題了,那么 就是我們并沒有指定當(dāng)前 Int 數(shù)據(jù)在排列過程中每個數(shù)據(jù)和每個數(shù)據(jù)之間的間隔是多少承耿?
代碼修改如下:

///advanced 移動對應(yīng)的步長冠骄,來存放內(nèi)容
let size = MemoryLayout<Int>.size
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment

let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * stride, alignment: alignment)

for i in 0 ..< 4 {
    p.advanced(by: i * stride).storeBytes(of: i, as: Int.self)
///(p + i * stride).storeBytes(of: i, as: Int.self)
}

print(p)

for i in 0 ..< 4 {
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index: \(i), value: \(value)")
}
p.deallocate()

lldb打印結(jié)果:

0x0000000105058e30
index: 0, value: 0
index: 1, value: 1
index: 2, value: 2
index: 3, value: 3

這才是正確的,符合我們期望的存儲內(nèi)容打印加袋。
其中

p.advanced(by: i * stride).storeBytes(of: i, as: Int.self)

也等價于

///p凛辣,基地址,i *stride职烧,移動對應(yīng)的步長位置來存放i
(p + i * stride).storeBytes(of: i, as: Int.self)

1.2.2 泛型指針的使用

這?的泛型指針相?較原?指針來說扁誓,其實就是指定當(dāng)前指針已經(jīng)綁定到了具體的類型。同樣的阳堕,我們 還是通過?個例?來解釋?下跋理。

var age = 18
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

lldb輸出打印

0x0000000100008058

我們得到了age這個變量的內(nèi)存指針。那么如果我們想要修改age這個變量可以怎么辦哪恬总?
在進(jìn)?泛型指針訪問的過程中前普,我們并不是使? loadstore ?法來進(jìn)?存儲操作。這?我們使?到當(dāng)前泛型指針內(nèi)置的變量 pointee壹堰。 獲取 UnsafePointer 的?式有兩種拭卿。
?種?式就是通過已有變量獲取,如下:

var age = 18
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

age = withUnsafePointer(to: age) { ptr in
    return ptr.pointee + 20
}
print(age)

發(fā)現(xiàn)更改后贱纠,它的存儲地址和存儲的值已經(jīng)發(fā)生改變峻厚。

0x0000000100008068
38

注意一點,這里我們無法直接修改ptr.pointee谆焊,如果想要修改惠桃,我們可以通過可變類型的MutablePointer,代碼如下:

var age = 18

withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

withUnsafeMutablePointer(to: &age) { ptr in
    ptr.pointee += 50
}
print(age)

lldb輸出打印結(jié)果:

x0000000100008068
68

還有一種方式就是直接分配內(nèi)存

var age = 18
///1、分配一塊Int類型的內(nèi)存空間辜王,注意這個時候當(dāng)前內(nèi)存空間還沒有被初始化
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
///2劈狐、為age initialize初始化分配的內(nèi)存空間
ptr.initialize(to: age)
///3、訪問當(dāng)前內(nèi)存的值呐馆,直接通過pointee屬性來進(jìn)行訪問
print(ptr.pointee)

這?我們看?張圖就?較清晰了:


泛型指針.png

下面我們操作一個結(jié)構(gòu)體來實踐一下肥缔。

struct ZGTeacher {
    var age: Int
    var height: Double
}

下面我們來嘗試看一下如何用指針來訪問這個結(jié)構(gòu)體。代碼如下:

struct ZGTeacher {
    var age: Int
    var height: Double
}

///capacity: 比如我們需要開辟5個內(nèi)存空間的大小
///allocate: 開辟空間
var ptr = UnsafeMutablePointer<ZGTeacher>.allocate(capacity: 5)
///初始化我們的指針內(nèi)存汹来,并存儲對應(yīng)的值
ptr[0] = ZGTeacher(age: 18, height: 20.0)
ptr[1] = ZGTeacher(age: 22, height: 30.0)

///defer關(guān)鍵字续膳,當(dāng)前程序運行完成后會執(zhí)行這塊代碼
defer {
    ///deinitialize回收5個內(nèi)存空間
    ptr.deinitialize(count: 5)
    ///銷毀對應(yīng)的內(nèi)存空間
    ptr.deallocate()
}

我們同樣還可以如下面代碼這樣初始化:

struct ZGTeacher {
    var age: Int = 18
    var height: Double = 1.85
}

let p = UnsafeMutablePointer<ZGTeacher>.allocate(capacity: 2)
p.initialize(to: ZGTeacher())
p.advanced(by: MemoryLayout<ZGTeacher>.stride).initialize(to: ZGTeacher(age: 20, height: 1.75))

defer {
    p.deinitialize(count: 2)
    p.deallocate()
}

1.2.3 內(nèi)存指針的使用

接下來,我們來嘗試用指針讀取Mach-o文件中屬性的名稱收班,代碼如下:

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var size: UInt = 0
///__swift5_types section 的pFile
var ptr = getsectdata("__TEXT", "__swift5_types", &size)

///pFile地址 0x0000000100007e24
print("ptr: \(ptr!)")

///獲取程序當(dāng)前運行基地址  0x0000000100000000
///mach_header

var excute_header: UnsafePointer<mach_header>?
let count = _dyld_image_count()

for i in 0..<count {
    let ptr = _dyld_get_image_header(i)
    if ptr!.pointee.filetype == MH_EXECUTE {
        excute_header = ptr
    }
}

var mhHeaderPtr = excute_header

print("程序當(dāng)前運行基地址mhHeaderPtr: \(mhHeaderPtr!)")


///Segment Name
var setCommond64Ptr = getsegbyname("__LINKEDIT")

///真實基地址
var linkBaseAddress: UInt64 = 0
///vmaddr虛擬基地址
///fileoff當(dāng)前文件的偏移量
///真實基地址 = 虛擬基地址 - 文件的偏移量
///linkBaseAddress 十進(jìn)制4294967296 十六進(jìn)制0x100000000
///十進(jìn)制轉(zhuǎn)十六進(jìn)制,返回的是字符串格式String(X,radix:16)

if let vmaddr = setCommond64Ptr?.pointee.vmaddr, let fileoff = setCommond64Ptr?.pointee.fileoff {
    linkBaseAddress = vmaddr - fileoff
//    print("真實基地址linkBaseAddress: \(linkBaseAddress)")
    print("真實基地址linkBaseAddress: " + "0x" + String(linkBaseAddress, radix: 16))
}

///offset = 當(dāng)前的pFile地址 - 真實基地址
///offset: 十進(jìn)制32292 十六進(jìn)制0x7e24
var offset: UInt64 = 0
if let unwrappedPtr = ptr {
    ///把當(dāng)前的地址信息轉(zhuǎn)換為UInt64類型
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    offset = intRepresentation - linkBaseAddress
//    print("偏移量: \(offset)")
    print("偏移量: " + "0x" + String(offset, radix: 16))
}

///將首地址ptr轉(zhuǎn)換為UInt64類型的坟岔,方便下面的計算
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
///存放內(nèi)容的真實地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + offset
/////將存放內(nèi)容的真實地址轉(zhuǎn)換為指針類型
//var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress) {print($0)}

///真實pfile存儲的value(內(nèi)容)地址
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee

///十進(jìn)制4294966164,十六進(jìn)制轉(zhuǎn)換一下0xfffffb94
//print("真實pfile存儲的value(內(nèi)容)地址 dataLoContent: \(dataLoContent!)")
print("真實pfile存儲的value(內(nèi)容)地址 dataLoContent: " + "0x" + String(dataLoContent!, radix: 16))

///描述文件pfile在Mach-O文件中的偏移信息
///當(dāng)前pfile真實地址偏移量 = 當(dāng)前pfile真實地址 + 偏移量 - 真實基地址
let typeDescOffset = UInt64(dataLoContent!) + offset -  linkBaseAddress

///當(dāng)前pfile真實地址 = UInt64類型的當(dāng)前pfile真實地址偏移量 + UInt64類型的真實基地址
var typeDescAdress = typeDescOffset + mhHeaderPtr_IntRepresentation
///十進(jìn)制4295014840闺阱,十六進(jìn)制轉(zhuǎn)換一下0x10000b9b8
//print("當(dāng)前pfile真實地址 typeDescAdress: \(typeDescAdress)")
print("當(dāng)前pfile真實地址 typeDescAdress: " + "0x" + String(typeDescAdress, radix: 16))

///為了打印typeDescAdress內(nèi)部結(jié)構(gòu)
struct TargetClassDescriptor {
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32
    var Offset: UInt32
    var size: UInt32
}

///將當(dāng)前pfile真實地址轉(zhuǎn)換為指向TargetClassDescriptor的指針類型炮车,并獲取它的pointee屬性
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAdress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    
//    print("類名偏移量 nameOffset: \(nameOffset)")
    print("類名偏移量 nameOffset: " + "0x" + String(nameOffset, radix: 16))
    
    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
    
//    print("類名地址 nameAddress: \(nameAddress)")
    print("類名地址 nameAddress: " + "0x" + String(nameAddress, radix: 16))
    
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)) {
        ///類名
        print("類名:" + String(cString: cChar))
    }
}


/// 獲取屬性
/// 獲取屬性相關(guān)的filedDescriptor 在運行中的內(nèi)存地址
let filedDescriptorRelaticveAddress = typeDescOffset + 4 * 4 + mhHeaderPtr_IntRepresentation

///十進(jìn)制4295014856,十六進(jìn)制轉(zhuǎn)換一下0x10000b9c8
//print("filedDescriptor的地址 filedDescriptorRelaticveAddress: \(filedDescriptorRelaticveAddress)")
print("filedDescriptor的地址 filedDescriptorRelaticveAddress: " + "0x" + String(filedDescriptorRelaticveAddress, radix: 16))

struct FieldDescriptor  {
    var mangledTypeName: Int32
    var superclass: Int32
    var Kind: UInt16
    var fieldRecordSize: UInt16
    var numFields: UInt32
//    var fieldRecords: [FieldRecord]
}

struct FieldRecord {
    var Flags: UInt32
    var mangledTypeName: Int32
    var fieldName: UInt32
}

///獲取fieldDescriptor 偏移量offset
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee

///fieldDescriptor的Offset偏移量的地址 十進(jìn)制656酣溃,十六進(jìn)制轉(zhuǎn)換一下0x290
//print("fieldDescriptor的Offset偏移量的地址 fieldDescriptorOffset: \(fieldDescriptorOffset!)")
print("fieldDescriptor的Offset偏移量的地址 fieldDescriptorOffset: " + "0x" + String(fieldDescriptorOffset!, radix: 16))
///獲取 FieldDescriptor 的在運行中的內(nèi)存地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)

///將 FieldDescriptor 的內(nèi)存地址直接轉(zhuǎn)換成指向 FieldDescriptor 結(jié)構(gòu)體的指針
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

///循環(huán)遍歷屬性
for i in 0 ..< fieldDescriptor!.numFields {
    ///FieldRecord 結(jié)構(gòu)體由 3個 4字節(jié)組成,并且保持3 * 4 = 12字節(jié)對齊
    let stride: UInt64 = UInt64(i * 3 * 4)
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
//    print(fieldRecordRelactiveAddress)
//    let fieldRecord = UnsafePointer<FieldRecord>.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)?.pointee
//    print(fieldRecord)
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
    let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
//    print(offset)
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - linkBaseAddress
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        print(String(cString: cChar))
    }
}

// 獲取v-table
// 函數(shù)的結(jié)構(gòu)體
struct TargetMethodDescriptor {
    var kind: UInt32
    var offset: UInt32
}

// 獲取方法的數(shù)量
if let methods = classDescriptor?.methods {
    for i in 0..<methods {
        // 獲取v-table的的首地址
        let VTableRelaticveAddress = typeDescOffset + 4 * 13 + mhHeaderPtr_IntRepresentation
        // 獲取當(dāng)前函數(shù)的地址
        let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
        // 將 當(dāng)前函數(shù) 的內(nèi)存地址直接轉(zhuǎn)換成指向 TargetMethodDescriptor 結(jié)構(gòu)體的指針
        let currentMethod = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: currentMethodAddress) ?? 0)?.pointee
        // 獲取到imp的地址
        let impAddress = currentMethodAddress + 4 + UInt64(currentMethod!.offset) - linkBaseAddress
        print(impAddress);
    }
}

1.3 內(nèi)存綁定

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

  • assumingMemoryBound(to:)
func testPointer(_ p: UnsafePointer<Int>) {
    print(p)
}

let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(tuplePtr)
}

編譯運行纪隙,發(fā)現(xiàn)報錯了赊豌,如下圖所示

指針訪問.png

那么想要順利訪問到元組的值我們可以怎么辦哪? 首先,第一點绵咱,我們先把我們的指針轉(zhuǎn)換為原生指針UnsafeRawPointer,然后我們調(diào)用assumingMemoryBound綁定成對應(yīng)類型。

func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0], p[1])
}

let tuple = (10, 20)

withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}

編譯一下,正確輸出打印元組的值 10容客,20
那么我們使用assumingMemoryBound的意義是什么翰意?
有時候我們的類型只有這種原生指針UnsafeRawPointer,或者說像UnsafePointer<(Int, Int)>UnsafePointer<Int>這種麸锉,兩種值類型相似钠绍,但是我們又不想經(jīng)過一系列的轉(zhuǎn)換操作來增加代碼復(fù)雜度,對我們的指針進(jìn)行生硬的轉(zhuǎn)換花沉,那么這個時候我們就可以使用這個API assumingMemoryBound來告訴我們的編譯器自己預(yù)期的類型柳爽,不需要編譯器再重復(fù)檢查(注意:這?只是讓編譯器繞過類型檢查,并沒有發(fā)?實際類型的轉(zhuǎn)換)碱屁。

  • bindMemory(to: , capacity: )
    調(diào)用bindMemory綁定成對應(yīng)類型磷脯,這里我們發(fā)生了實際類型的轉(zhuǎn)換。如果當(dāng)前的內(nèi)存沒有綁定類型娩脾,那么我們就首次綁定類型赵誓,如果有它當(dāng)前的原有類型,那么調(diào)用這個API,我們重新綁定為指定類型俩功。
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0], p[1])
}

let tuple = (10, 20)

withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}

這里我們將UnsafePointer<(Int, Int)>轉(zhuǎn)換成了UnsafePointer<Int>類型幻枉。

  • withMemoryRebound(to: , capacity: )
    withMemoryRebound就是用來臨時更改我們的類型,減少代碼復(fù)雜度绑雄。
func testPoint(_ p: UnsafePointer<Int8>) {

}

let UInt8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
UInt8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
    testPoint(int8Ptr)
})

二展辞、內(nèi)存管理

swift中使??動引?計數(shù)(ARC)機(jī)制來追蹤和管理內(nèi)存。

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t = ZGTeacher()
///固定寫法万牺,打印這個t實例的內(nèi)存指針
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())
print("end")

打印輸出

0x0000000101252a90

我們用x/8g指令輸出一下

(lldb) x/8g 0x0000000101252a90
0x101252a90: 0x0000000100008198 0x0000000000000003
0x101252aa0: 0x0000000000000012 0x000000676e61685a
0x101252ab0: 0xe500000000000000 0x0000000000000000
0x101252ac0: 0x00000009a0080001 0x00007ff84be59aa0

我們拿到了這個實例對象t的內(nèi)存指針地址罗珍,我們知道實例對象內(nèi)存地址其中的前16個字節(jié)的后8個字節(jié)在這個過程當(dāng)中本質(zhì)上是存儲我們的refCounts,但我們不知道它存儲的到底代表什么意思脚粟,現(xiàn)在我們在Swift源碼里面看一下覆旱。
首先我們先找到refCount的定義,這里我們在HeapObject.h文件中搜索

// The members of the HeapObject header that are not shared by a
 // standard Objective-C instance
 #define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
   InlineRefCounts refCounts

 /// The Swift heap-object header.
 /// This must match RefCountedStructTy in IRGen.
 struct HeapObject {
   /// This is always a valid pointer to a metadata object.
   HeapMetadata const *__ptrauth_objc_isa_pointer metadata;

   SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

 #ifndef __swift__
   HeapObject() = default;

   // Initialize a HeapObject header as appropriate for a newly-allocated object.
   constexpr HeapObject(HeapMetadata const *newMetadata)
     : metadata(newMetadata)
     , refCounts(InlineRefCounts::Initialized)
   { }

我們看到refCounts是由InlineRefCounts定義的核无,接下來扣唱,我們沿著這個InlineRefCounts定義點擊進(jìn)去。

typedef RefCounts<InlineRefCountBits> InlineRefCounts;
 typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

可以看到InlineRefCounts其實是一個模版類团南,接受一個泛型參數(shù)InlineRefCountBits噪沙。

template <typename RefCountBits>
 class RefCounts {
   std::atomic<RefCountBits> refCounts;

   // Out-of-line slow paths.

   SWIFT_NOINLINE
   void incrementSlow(RefCountBits oldbits, uint32_t inc) SWIFT_CC(PreserveMost);

   SWIFT_NOINLINE
   void incrementNonAtomicSlow(RefCountBits oldbits, uint32_t inc);

   SWIFT_NOINLINE
   bool tryIncrementSlow(RefCountBits oldbits);

   ......
   }

可以看到,本質(zhì)上它在操作我們的API的時候都操作的是我們的泛型參數(shù)RefCountBits吐根。InlineRefCountBits它其實是一個模版類正歼,RefCounts本質(zhì)上是對我們當(dāng)前引用計數(shù)的一個包裝,我們引用計數(shù)的具體類型取決于傳進(jìn)來的參數(shù)RefCountBits拷橘。

// Basic encoding of refcount and flag data into the object's header.
 template <RefCountInlinedness refcountIsInline>
 class RefCountBitsT {

   friend class RefCountBitsT<RefCountIsInline>;
   friend class RefCountBitsT<RefCountNotInline>;
   
   static const RefCountInlinedness Inlinedness = refcountIsInline;

   typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
     BitsType;
   typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::SignedType
     SignedBitsType;
   typedef RefCountBitOffsets<sizeof(BitsType)>
     Offsets;

   BitsType bits;

點擊Type看一下類型局义,它是一個uint64_t類型的位域信息

template <>
 struct RefCountBitsInt<RefCountNotInline, 4> {
   typedef uint64_t Type;
   typedef int64_t SignedType;
 };

看到這里我們可以得出結(jié)論,我們的引用計數(shù)它是一個64位的位域信息冗疮。在這個位域信息里存儲了和我們當(dāng)前這個運行生命周期相關(guān)的引用計數(shù)萄唇。
當(dāng)我們創(chuàng)建一個實例對象的時候,當(dāng)前的引用計數(shù)是多少术幔?
我們先從源代碼中找一下它的初始化方法new (object) HeapObject(metadata)

static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask));

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata);

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}

點擊HeapObject方法另萤,我們看到了它的初始化賦值方法refCounts(InlineRefCounts::Initialized)

// Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

點擊或者搜索Initialized,可以看到以下代碼:

public:
  enum Initialized_t { Initialized };
  enum Immortal_t { Immortal };

  // RefCounts must be trivially constructible to avoid ObjC++
  // destruction overhead at runtime. Use RefCounts(Initialized)
  // to produce an initialized instance.
  RefCounts() = default;
  
  // Refcount of a new object is 1.
  constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}

  // Refcount of an immortal object has top and bottom bits set
  constexpr RefCounts(Immortal_t)
  : refCounts(RefCountBits(RefCountBits::Immortal)) {}
  

RefCountBits就是我們剛才講的模版類特愿。想要了解這個類那么我們就去搜索一下RefCountBitsT

SWIFT_ALWAYS_INLINE
  constexpr
  RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
    : bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
           (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
           (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
  { }

strongExtraCount 傳入的0仲墨,StrongExtraRefCountShift 在33位,unownedCount 傳入的是1揍障,UnownedRefCountShift 1位,
0左移33位是0目养,1左移1位是2。
接下來我們來看一下當(dāng)我們做一個強(qiáng)引用的時候它的引用計數(shù)是怎么變化的哪毒嫡?

class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t = ZGTeacher()
///固定寫法癌蚁,打印這個t實例的內(nèi)存指針
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

var t1 = t
var t2 = t
print("end")
image.png

我們逐步放開斷點幻梯,并用x/8g指令打印輸出,看到它的強(qiáng)引用計數(shù)變?yōu)榱?努释。通過位移運算左移33位碘梢,高34位,每次加2伐蒂。
我們來看一下位域布局圖

位域.png

  • 1 ~31位存儲的是無主引用
  • 32位存儲的是當(dāng)前的類是否正在析構(gòu)
  • 33 ~ 62位存儲的是強(qiáng)引用
    我們簡單用代碼來驗證一下第32位信息
class ZGTeacher {
    var age: Int = 18
    var name: String = "Zhang"
}

var t: ZGTeacher? = ZGTeacher()
///固定寫法煞躬,打印這個t實例的內(nèi)存指針
print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

t = nil

print("end")
析構(gòu).png

32位為1,表示正在析構(gòu)逸邦。
那么引用計數(shù)是如何操作強(qiáng)引用的哪恩沛,我們也可以到源碼里看一下。

SWIFT_ALWAYS_INLINE
 static HeapObject *_swift_retain_(HeapObject *object) {
   SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
   if (isValidPointerForNativeRetain(object))
     object->refCounts.increment(1);
   return object;
 }
SWIFT_ALWAYS_INLINE
   void increment(uint32_t inc = 1) {
     auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
     
     // constant propagation will remove this in swift_retain, it should only
     // be present in swift_retain_n
     if (inc != 1 && oldbits.isImmortal(true)) {
       return;
     }
     
     RefCountBits newbits;
     do {
       newbits = oldbits;
       bool fast = newbits.incrementStrongExtraRefCount(inc);
       if (SWIFT_UNLIKELY(!fast)) {
         if (oldbits.isImmortal(false))
           return;
         return incrementSlow(oldbits, inc);
       }
     } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                               std::memory_order_relaxed));
   }
SWIFT_NODISCARD SWIFT_ALWAYS_INLINE bool
   incrementStrongExtraRefCount(uint32_t inc) {
     // This deliberately overflows into the UseSlowRC field.
     bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
     return (SignedBitsType(bits) >= 0);
   }

這里也是通過位域運算得來的缕减。使用強(qiáng)引用就會造成一個問題:循環(huán)引用雷客。我們來看一下以下案例:

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var subject: ZGTeacher?
}

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

var t = ZGTeacher()

var subject = ZGSubject.init("swift", t)

t.subject = subject

這里實例對象相互持有,造成對象無法釋放桥狡,產(chǎn)生了循環(huán)引用搅裙。那么我們怎么解決這一問題哪?在swit中有兩種方式裹芝,第一種是我們的弱引用部逮,第二種方式是無主引用Unowned。

2.1 弱引用

弱引?不會對其引?的實例保持強(qiáng)引?嫂易,因?不會阻? ARC 釋放被引?的實例甥啄。這個特性阻?了引?變?yōu)檠h(huán)強(qiáng)引?。聲明屬性或者變量時炬搭,在前?加上 weak 關(guān)鍵字表明這是?個弱引?。 由于弱引?不會強(qiáng)保持對實例的引?穆桂,所以說實例被釋放了弱引?仍舊引?著這個實例也是有可能的宫盔。 因此,ARC 會在被引?的實例被釋放時?動地設(shè)置弱引?為 nil 享完。由于弱引?需要允許它們的值為 nil 灼芭, 它們?定得是可選類型。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
}

weak var t = ZGTeacher()

print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

print("end")

weak var t = ZGTeacher()加上斷點般又,并打開Xcode的匯編調(diào)試彼绷,編譯運行一下

image.png

發(fā)現(xiàn)它明顯執(zhí)行了一個swift_weakInit。我們到swift源碼里來搜索一下這個方法茴迁。

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);
  }
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // Preflight failures before allocating a new side table.
  if (oldbits.hasSideTable()) {
    // Already have a side table. Return it.
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    // Already past the start of deinit. Do nothing.
    return nullptr;
  }

  // Preflight passed. Allocate a side table.
  
  // FIXME: custom side table allocator
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  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;
}

HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

在我們的swift里面,本質(zhì)上存在著兩種引用計數(shù)堕义,一種是InlineRefCounts猜旬,里面就是strong RC + unowned RC + flags,如果我們當(dāng)前是有我們的引用計數(shù),此時它就存儲了HeapObjectSideTableEntry洒擦,里面包含了strong RC + unowned RC + weak RC + flags椿争。

截屏2022-01-12 17.52.16.png

截屏2022-01-12 17.52.28.png
截屏2022-01-12 17.52.43.png

截屏2022-01-12 17.55.34.png
截屏2022-01-12 17.55.41.png

可以看到這里的本質(zhì)是用64位的指針,把當(dāng)前的side存儲到64位位域中熟嫩,并設(shè)置一些標(biāo)記位秦踪。

Side Table 是?種類名為 HeapObjectSideTableEntry 的結(jié)構(gòu),??也有 RefCounts 成員掸茅,內(nèi)部是 SideTableRefCountBits椅邓,其實就是原來的 uint64_t 加上?個存儲弱引?數(shù)的 uint32_t

截屏2022-01-12 17.57.43.png

2.2 Unowned

和弱引?類似倦蚪,?主引?不會牢牢保持住引?的實例希坚。但是不像弱引?,總之陵且,?主引?假定是永遠(yuǎn)有值裁僧。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
}

var t: ZGTeacher?
t = ZGTeacher()

print(Unmanaged.passUnretained(t as AnyObject).toOpaque())

unowned var t1 = t

t = nil

print(t1)

這里產(chǎn)生了崩潰,因為unowned修飾的t1必須是假定有值的慕购,t或者t1為nil就產(chǎn)生了崩潰聊疲。
當(dāng)我們知道兩個對象的?命周期并不相關(guān),那么我們必須使? weak沪悲。相反获洲,?強(qiáng)引?對象擁有和強(qiáng)引?對象同樣或者更?的?命周期的話,則應(yīng)該使? unowned殿如。
我們來看一下這個例子:

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var subject: ZGSubject?
}

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

var t = ZGTeacher()

var subject = ZGSubject.init("swift", t)

t.subject = subject

這個過程中我們的Teacher和SubjectName贡珊,如果老師不在了,那么這個過程中所講授的課程也就不在了涉馁。
這個過程中我們可以通過unowned來解決這個循環(huán)引用门岔,也就意味著當(dāng)前的生命周期ZGTeacher更長。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var subject: ZGSubject?
}

class ZGSubject {
    var subjectName: String
    ///這里添加 unowned
    unowned  var subjectTeacher: ZGTeacher
    init(_ subjectName: String, _ subjectTeacher: ZGTeacher) {
        self.subjectName = subjectName
        self.subjectTeacher = subjectTeacher
    }
}

var t = ZGTeacher()

var subject = ZGSubject.init("swift", t)

t.subject = subject

2.3 Weak VS Unowned比較

注意weak相對于unowned更安全烤送,但是unowned相對于weak性能更好寒随。因為weak需要重新創(chuàng)建一個sideTable散列表,還要對我們當(dāng)前的sideTable散列表進(jìn)行操作帮坚,而unowned直接操作了我們64的信息妻往,但是在使用過程中要確保unowned是有值的。

2.4 閉包循環(huán)引用

var age = 18
let closure = {
    age += 1
}

closure()
print(age)

lldb輸出打印結(jié)果

19

我們的閉包會?般默認(rèn)捕獲我們外部的變量试和,閉包內(nèi)部對變量的修改將會改變外部原始變量的值讯泣。
下面的案例就是閉包循環(huán)引用的經(jīng)典案例,對象無法釋放灰署。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var testClosure: (() -> ())?
    deinit {
        print("ZGTeacher deinit")
    }
}

func test() {
    let t = ZGTeacher()
    t.testClosure = {
        t.age += 1
    }
    
    print("end")
}

test()

那么我們?nèi)绾谓鉀Q這一問題哪判帮?可以使用我們當(dāng)前的捕獲列表局嘁。

class ZGTeacher {
    var age: Int = 18
    var name: String = "zhang"
    var testClosure: (() -> ())?
    deinit {
        print("ZGTeacher deinit")
    }
}

func test() {
    let t = ZGTeacher()
    t.testClosure = { [weak t] in
        t!.age += 1
    }
    
    print("end")
}

test()

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市晦墙,隨后出現(xiàn)的幾起案子悦昵,更是在濱河造成了極大的恐慌,老刑警劉巖晌畅,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件但指,死亡現(xiàn)場離奇詭異,居然都是意外死亡抗楔,警方通過查閱死者的電腦和手機(jī)棋凳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來连躏,“玉大人剩岳,你說我怎么就攤上這事∪肴龋” “怎么了拍棕?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長勺良。 經(jīng)常有香客問我绰播,道長,這世上最難降的妖魔是什么尚困? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任蠢箩,我火速辦了婚禮,結(jié)果婚禮上事甜,老公的妹妹穿的比我還像新娘谬泌。我一直安慰自己,他們只是感情好逻谦,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布呵萨。 她就那樣靜靜地躺著,像睡著了一般跨跨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上囱皿,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天勇婴,我揣著相機(jī)與錄音,去河邊找鬼嘱腥。 笑死耕渴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的齿兔。 我是一名探鬼主播橱脸,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼础米,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了添诉?” 一聲冷哼從身側(cè)響起屁桑,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栏赴,沒想到半個月后蘑斧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡须眷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年竖瘾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片花颗。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡捕传,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扩劝,到底是詐尸還是另有隱情庸论,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布今野,位于F島的核電站葡公,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏条霜。R本人自食惡果不足惜催什,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宰睡。 院中可真熱鬧蒲凶,春花似錦、人聲如沸拆内。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽麸恍。三九已至灵巧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抹沪,已是汗流浹背刻肄。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留融欧,地道東北人敏弃。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像噪馏,于是被迫代替她去往敵國和親麦到。 傳聞我的和親對象是個殘疾皇子绿饵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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