一:指針
1. 指針的定義
Swift
中引用了某個引用類型實例的常量或變量,與 C
語言中的指針類似,不過它并不直接指向某個內存地址几于,也不要求你使用星號(*)
來表明你在創(chuàng)建一個引用讳侨。相反,Swift
中引用的定義方式與其它的常量或變量的一樣戳杀。
指針是不安全的:
- 比如我們在創(chuàng)建一個對象的時候,是需要在堆分配內存空間的。但是這個內存空間的生命周期是有限的桂敛,也就意味著如果我們使用指針指向這塊內存空間功炮,如果當前內存空間的生命周期到了(引用計數(shù)變?yōu)?),那么我們當前的指針就變成了未定義的行為了埠啃,也就變成了野指針死宣。
- 創(chuàng)建的內存空間是越界的,比如我創(chuàng)建了一個大小為
10
的數(shù)組碴开,這個時候我們通過指針訪問到了index = 11
的位置毅该,這個時候就數(shù)組越界了,訪問了一個未知的內存空間潦牛。 - 指針所指向的類型與內存的值類型不一致眶掌,也是不安全的。
2. 指針類型
Swift
中的指針分為兩類
-
typed pointer
指定數(shù)據(jù)類型指針 -
raw pointer
未指定數(shù)據(jù)類型的指針(原生指針)
基本上我們接觸的指針有以下幾種
Swift的指針和OC的指針對比.png
2.1 原生指針
首先來了解一下步長信息
struct Person {
var age: Int = 18
var sex: Bool = true
}
print(MemoryLayout<Person>.size) // 真實大小
print(MemoryLayout<Person>.stride) // 步長信息
print(MemoryLayout<Person>.alignment) // 對齊信息
// 打印結果
9 // 8(int) + 1(bool)
16 // 8 + 8 bool雖然只占用一個字節(jié)
8
我們可以看到
-
size
的結果是9
=int
的8
字節(jié) +bool
的1
字節(jié) -
stride
的結果是16
, 因為alignment
的值為8
巴碗,也就是說是按照8
字節(jié)對齊朴爬,所以步長信息為8
+8
=16
字節(jié)。
接下來使用原生指針 (Raw Pointer)
存儲4個整型的數(shù)據(jù)
示例代碼
// 首先開辟一塊內存空間 byteCount: 當前總的字節(jié)大小 4 x 8 = 32 alignment: 對齊的大小
let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)
for i in 0..<4 {
// 調用 advanced 獲取到每個地址排列的過程中應該距離首地址的大小 i x MemoryLayout<Int>.stride
// 調用 store 方法存儲當前的整型數(shù)值
p.advanced(by:i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}
for i in 0..<4 {
// 調用 load 方法加載當前指針當中對應的內存數(shù)據(jù)
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index--\(i), value--\(value)")
}
// 釋放創(chuàng)建的連續(xù)的內存空間
p.deallocate()
// 打印結果
index--0, value--0
index--1, value--1
index--2, value--2
index--3, value--3
2.2 類型指針
類型指針相較于原生指針來說橡淆,其實就是指定當前指針已經綁定到了具體的類型召噩,在進行類型指針訪問的過程中,我們不再使用 store
和 load
方法進行存儲操操作逸爵,而是直接使用類型指針內置的變量 pointee
獲取 UnsafePointer
有兩種方式
- 通過已有變量獲取
var age = 18
// 通過 withUnsafePointer 來訪問到當前變量的地址
withUnsafePointer(to: &age) { ptr in
print(ptr)
}
age = withUnsafePointer(to: &age) { ptr in
//注意這里我們不能直接修改ptr.pointee
return ptr.pointee + 12
}
var b = 18
// 使用mutable修改ptr.pointee
withUnsafeMutablePointer(to: &b) { ptr in
ptr.pointee += 10
print(ptr)
}
- 直接分配內存
var age = 10
// 分配一塊int類型內存空間, 注意當前內存空間還沒被初始化
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// 初始化分配內存空間
tPtr.initialize(to: age)
// 訪問當前內存的值, 直接通過pointee進行訪問
print(tPtr.pointee)`
類型指針主要涉及到的api主要有
示例
struct Person {
var age = 18
var name = "小明"
}
// 方式一
// capacity 內存空間 5個連續(xù)的內存空間
var tptr = UnsafeMutablePointer<Person>.allocate(capacity: 5)
// tptr就是當前分配的內存空間的首地址
tptr[0] = Person.init(age: 18, name: "小明")
tptr[1] = Person.init(age: 19, name: "小強")
// 這兩個是成對出現(xiàn)的
// 清除內存空間中內容
tptr.deinitialize(count: 5)
// 回收內存空間
tptr.deallocate()
// 方式二
// 開辟2個連續(xù)的內存空間
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)
p.initialize(to: Person())
p.advanced(by: MemoryLayout<Person>.stride).initialize(to: Person(age: 18, name: "小明"))
// 當前程序運行完成后 執(zhí)行defer
defer {
// 這兩個是成對出現(xiàn)的
p.deinitialize(count: 2)
p.deallocate()
}
2.3 內存指針的使用-內存綁定
Swift
提供了三種不同的API來綁定/重新綁定指針:
-
assumingMemoryBound(to:)
有些時候我們處理代碼的過程中只有原生指針(沒有報錯指針類型)具滴,但此刻對于處理代碼的的我們來說明確知道指針的類型,我們就可以使用assumingMemoryBound(to:)
來告訴編譯器預期的類型师倔。
(注意:這里只是讓編譯器繞過類型檢查构韵,并沒有發(fā)生實際的類型轉換)
func testPointer(_ p: UnsafePointer<Int>) {
print(p[0])
print(p[1])
}
// 這里的元祖是值類型,本質上這塊內存空間中存放的就是Int類型的數(shù)據(jù)
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
// 先將tuplePtr 轉換成原生指針趋艘, 在調用assumingMemoryBound(to:) 告訴編譯器當前內存已經綁定過Int了疲恢,這個時候編譯器就不會進行檢查
testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
-
bindMemory(to: capacity:)
用于更改內存綁定的類型,如果當前內存還沒有類型綁定瓷胧,則將首次綁定為該類型显拳,否則重新綁定該類型,并且內存中所有的值都會變成該類型
func testPointer(_ p: UnsafePointer<Int>) {
print(p[0])
print(p[1])
}
// 這里的元祖是值類型抖单,本質上這塊內存空間中存放的就是Int類型的數(shù)據(jù)
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
// 先將tuplePtr 轉換成原生指針萎攒, 將原生指針轉換成UnsafePointer<Int>類型
testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
-
withMemoryRebound(to: capacity: body:)
當我們在給外部函數(shù)傳遞參數(shù)時,不免會有一些數(shù)據(jù)類型上的差距矛绘,如果我們進行類型轉換耍休,必然要來會復制數(shù)據(jù),這個時候就可以調用withMemoryRebound(to: capacity: body:)
來臨時更改內存綁定類型货矮。
func testPointer(_ p: UnsafePointer<Int8>) {
print(p[0])
print(p[1])
}
let uint8Ptr = UnsafePointer<uint8>.init(bitPattern: 10)
// 減少代碼復雜度
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (int8Ptr: UnsafePointer<Int8>) in
testPointer(int8Ptr)
}
3.利用指針還原Macho文件中的屬性和函數(shù)表
class Person {
var age: Int = 18
var name: String = "小明"
}
var size: UInt = 0
//__swift5_types section 的pFile
var typesPtr = getsectdata("__TEXT", "__swift5_types", &size)
// 獲取當前程序運行地址 相當于 LLDB 中 image list 命令
var mhHeaderPtr = _dyld_get_image_header(0)
// 獲取 __LINKEDIT 中的內容 其中 getsegbyname 返回的是 UnsafePointer<segment_command_64>, segment_command_64 就包含了 vmaddr(虛擬內存地址) 和 fileoff(偏移量)
var setCommond64LinkeditPtr = getsegbyname("__LINKEDIT")
// 計算鏈接的基地址
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64LinkeditPtr?.pointee.vmaddr, let fileOff = setCommond64LinkeditPtr?.pointee.fileoff{
linkBaseAddress = vmaddr - fileOff
}
// 或者 直接去 LC_SEGMENT_64(__PAGEZERO)中的VM Size
var setCommond64PageZeroPtr = getsegbyname("__PAGEZERO")
if let vmsize = setCommond64PageZeroPtr?.pointee.vmsize {
linkBaseAddress = vmsize
}
// 獲取__TEXT, __swift5_types 在Macho中的偏移量
var typesOffSet: UInt64 = 0
if let unwrappedPtr = typesPtr {
// 將當前的地址信息轉換成UInt64
let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
typesOffSet = intRepresentation - linkBaseAddress
}
// 程序運行的首地址 轉換成UInt64類型
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))
// DataLo的內存地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + typesOffSet
// 轉換成指針類型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}
// 獲取dataLo指針指向的內容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee
// 獲取typeDescriptor的偏移量
let typeDescOffset = UInt64(dataLoContent!) + typesOffSet - linkBaseAddress
// 獲取typeDescriptor在程序運行中的地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation
// typeDescriptor結構體
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 methods: UInt32
}
// 將 typeDescriptor 的內存地址直接轉換成指向 TargetClassDescriptor 結構體的指針
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee
if let name = classDescriptor?.name {
// 獲取name的偏移量地址
let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
// 獲取name在運行中的內存地址
let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
print(String(cString: cChar))
}
}
// 獲取屬性
// 獲取屬性相關的filedDescriptor 在運行中的內存地址
let filedDescriptorRelaticveAddress = typeDescOffset + 4 * 4 + mhHeaderPtr_IntRepresentation
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 指針在的內容 就是FieldDescriptor 的偏移量
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee
// 獲取 FieldDescriptor 的在運行中的內存地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)
// 將 FieldDescriptor 的內存地址直接轉換成指向 FieldDescriptor 結構體的指針
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
// 循環(huán)遍歷屬性
for i in 0..<fieldDescriptor!.numFields{
// FieldRecord 結構體由 3個 4字節(jié)組成羊精,并且保持3 * 4 = 12字節(jié)對齊
let stride: UInt64 = UInt64(i * 3 * 4)
// 計算 fieldRecord 的地址
let fieldRecordAddress = fieldDescriptorAddress + stride + 16
// 計算 fieldRecord 結構體中的 name 在程序運行中的內存地址
let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
// 將上面地址的地址轉換成指針,并且獲取指向的內容 (偏移量)
let nameOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
// 獲取 name 的地址
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(nameOffset!) - linkBaseAddress
// 將 name 地址轉換成指針
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
// 打印指針內容
print(String(cString: cChar))
}
}
// 獲取v-table
// 函數(shù)的結構體
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
// 獲取當前函數(shù)的地址
let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
// 將 當前函數(shù) 的內存地址直接轉換成指向 TargetMethodDescriptor 結構體的指針
let currentMethod = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: currentMethodAddress) ?? 0)?.pointee
// 獲取到imp的地址
let impAddress = currentMethodAddress + 4 + UInt64(currentMethod!.offset) - linkBaseAddress
print(impAddress);
}
}
注意: 在 Xcode 13
中 _dyld_get_image_header(0)
對比在 LLDB
中輸入命令 image list
,發(fā)現(xiàn)沒有正確獲取到程序運行的基地址喧锦,但是在 Xcode 12
中不會出現(xiàn)這樣的問題读规。
發(fā)現(xiàn)
_dyld_get_image_header(0)
獲取到的地址是 image list
中第三個元素的地址,目前還沒找到解決辦法燃少,如果您正好知道請留意或者私信我束亏,萬分感謝。
- 經過后面的研究這里找到一個方式獲取當前程序運行的基地址
var mhHeaderPtr: UnsafePointer<mach_header>?
let count = _dyld_image_count()
for i in 0..<count {
var excute_header = _dyld_get_image_header(i)
if excute_header!.pointee.filetype == MH_EXECUTE {
mhHeaderPtr = excute_header
break
}
}
就是循環(huán)遍歷 _dyld_get_image_header
中的元素判斷是不是 mach-o
的執(zhí)行地址阵具。
二:內存管理
Swift
中使用自動引用計數(shù)(ARC)機制來追蹤和管理內存碍遍,通常情況下,Swift
內存管理機制會一直起作用阳液,你無須自己來考慮內存的管理怕敬。ARC
會在類的實例不再被使用時,自動釋放其占用的內存帘皿。
1. 強引用
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var p1: Person?
var p2: Person?
var p3: Person?
p1 = Person(name: "小明")
// 打印結果
小明 is being initialized
由于 Person
類的新實例被賦值給了 p1
變量东跪,所以 p1
到 Person
類的新實例之間建立了一個強引用。正是因為這一個強引用鹰溜,ARC
會保證 Person
實例被保持在內存中不被銷毀虽填。
我們接著添加代碼
p2 = p1
p3 = p1
現(xiàn)在這一個 Person
實例已經有三個強引用了。
將其中兩個變量賦值 nil
的方式斷開兩個強引用(包括最先的那個強引用)曹动,只留下一個強引用卤唉,Person
實例不會被銷毀
p1 = nil
p2 = nil
只有當最后一個引用被斷開時 ARC
才會銷毀它
p3 = nil
// 打印結果
小明 is being deinitialized
2. 弱引用
弱引用不會對其引用的實例保持強引用,因而不會阻止 ARC
銷毀被引用的實例仁期。這個特性阻止了引用變?yōu)檠h(huán)強引用。聲明屬性或者變量時竭恬,在前面加上 weak
關鍵字表明這是一個弱引用跛蛋。
因為弱引用不會保持所引用的實例,即使引用存在痊硕,實例也有可能被銷毀赊级。因此,ARC
會在引用的實例被銷毀后自動將其弱引用賦值為 nil
岔绸。并且因為弱引用需要在運行時允許被賦值為 nil
理逊,所以它們一定是可選類型。
class Person {
var age: Int = 18
var name: String = "小明"
}
weak var t = Person()
進入?yún)R編代碼
我們可以看到這里的實質是調用了
swift_weakInit
函數(shù)盒揉,根據(jù) Swift 源碼的分析晋被,其內部實現(xiàn)其實就是:一個對象在初始化的時候后是沒有 SideTable
(散列表)的,當我們創(chuàng)建一個弱引用的時候刚盈,系統(tǒng)會創(chuàng)建一個 SideTable
實質上
Swift
存在兩種引用計算的布局方式
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
}
}
}
其中
-
InlineRefCounts
和SideTableRefCounts
共享當前模板類RefCounts<T>.
的實現(xiàn)羡洛。 -
InlineRefCountBits
和SideTableRefCountBits
共享當前模板類RefCountBitsT<bool>
-
InlineRefCounts
其實是一個uint64_t
可以當引用計數(shù)也可以當Side Table
的指針 -
SideTableRefCounts
是一種名為HeapObjectSideTableEntry
的結構體,里面也有RefCounts
成員藕漱,內部是SideTableRefCountBits
欲侮,其實就是原來的uint64_t
加上一個存儲弱引用數(shù)的uint32_t
3. 無主引用
和弱引用類似崭闲,無主引用不會牢牢保持住引用的實例。和弱引用不同的是威蕉,無主引用在其他實例有相同或者更長的生命周期時使用刁俭。你可以在聲明屬性或者變量時,在前面加上關鍵字 unowned
表示這是一個無主引用韧涨。
但和弱引用不同牍戚,無主引用通常都被期望擁有值。所以氓奈,將值標記為無主引用不會將它變?yōu)榭蛇x類型翘魄,ARC
也不會將無主引用的值設置為 nil
∫蹋總之一句話就是暑竟,無主引用假定是永遠有值的。
- 如果兩個對象的生命周期完全和對方沒關系(其中一方什么時候賦值為
nil
育勺,對對方沒有影響)但荤,使用weak
- 如果能確保:其中一個對象銷毀,另一個對象也要跟著銷毀涧至,這時候可以(謹慎)使用
unowned
4. 閉包循環(huán)引用
閉包會一般默認捕獲外部的變量
var age = 18
let closure = {
age += 1
}
closure()
print(age)
// 打印結果
19
可以看出 閉包的內部對變量的修改將會改變外部原始變量的值
class Person {
var age: Int = 18
var name: String = "小明"
var testClosure:(() -> ())?
deinit {
print("Person deinit")
}
}
func testARC() {
let t = Person()
t.testClosure = {
print(t.age)
}
print("end")
}
testARC()
// 打印結果
end
我們發(fā)現(xiàn)沒有打印 Person deinit
腹躁,也就意味著 t
并沒有被銷毀,此時出現(xiàn)了循環(huán)引用南蓬。解決辦法:就是使用捕獲列表
func testARC() {
let t = Person()
t.testClosure = { [weak t] in
t?.age += 1
}
// t.testClosure = { [unowned t] in
// t.age += 1
// }
}
5. 捕獲列表
默認情況下纺非,閉包表達式從起周圍的范圍捕獲常量和變量,并強引用這些值赘方∩沼保可以使用捕獲列表來顯式控制如何在閉包中捕獲值。
在參數(shù)列表之前窄陡,捕獲列表被寫為用逗號括起來的表達式列表炕淮,并用方括號括起來。如果使用捕獲列表跳夭,則即使省略參數(shù)名稱涂圆,參數(shù)類型和返回類型,也必須使用 in
關鍵字币叹。
創(chuàng)建閉包時润歉,將初始化捕獲列表中的條目。對于捕獲列表中的每個條目颈抚,將常量初始化為在周圍范圍內具有相同名稱的常量或變量的值卡辰。
var age = 0
var height = 0.0
let closure = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
closure()
// 打印結果
0
1.85
創(chuàng)建閉包時,內部作用域中的 age
會用外部作用域中的 age
的值進行初始化,但他們的值未以任何特殊方式連接九妈。這意味著更改外部作用域中的 age
的值不會影響內部作用域中的 age
的值反砌,也不會更改封閉內部的值,也不會影響封閉外的值萌朱。先比之下宴树,只有一個名為 height
的變量-外部作用域中的 height
- 因此,在閉包內部或外部進行的更改在兩個均可見晶疼。