一:指针
1. 指针的定义
Swift
中引用了某个引用类型实例的常量或变量,与 C
语言中的指针类似,不过它并不直接指向某个内存地址,也不要求你使用星号(*)
来表明你在创建一个引用。相反,Swift
中引用的定义方式与其它的常量或变量的一样。
指针是不安全的:
- 比如我们在创建一个对象的时候,是需要在堆分配内存空间的。但是这个内存空间的生命周期是有限的,也就意味着如果我们使用指针指向这块内存空间,如果当前内存空间的生命周期到了(引用计数变为0),那么我们当前的指针就变成了未定义的行为了,也就变成了野指针。
- 创建的内存空间是越界的,比如我创建了一个大小为
10
的数组,这个时候我们通过指针访问到了index = 11
的位置,这个时候就数组越界了,访问了一个未知的内存空间。 - 指针所指向的类型与内存的值类型不一致,也是不安全的。
2. 指针类型
Swift
中的指针分为两类
-
typed pointer
指定数据类型指针 -
raw pointer
未指定数据类型的指针(原生指针)
基本上我们接触的指针有以下几种
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虽然只占用一个字节
8
我们可以看到
-
size
的结果是9
=int
的8
字节 +bool
的1
字节 -
stride
的结果是16
, 因为alignment
的值为8
,也就是说是按照8
字节对齐,所以步长信息为8
+8
=16
字节。
接下来使用原生指针 (Raw Pointer)
存储4个整型的数据
示例代码
// 首先开辟一块内存空间 byteCount: 当前总的字节大小 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 方法存储当前的整型数值
p.advanced(by:i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}
for i in 0..<4 {
// 调用 load 方法加载当前指针当中对应的内存数据
let value = p.load(fromByteOffset: i * 8, as: Int.self)
print("index--\(i), value--\(value)")
}
// 释放创建的连续的内存空间
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个连续的内存空间
var tptr = UnsafeMutablePointer<Person>.allocate(capacity: 5)
// tptr就是当前分配的内存空间的首地址
tptr[0] = Person.init(age: 18, name: "小明")
tptr[1] = Person.init(age: 19, name: "小强")
// 这两个是成对出现的
// 清除内存空间中内容
tptr.deinitialize(count: 5)
// 回收内存空间
tptr.deallocate()
// 方式二
// 开辟2个连续的内存空间
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)
p.initialize(to: Person())
p.advanced(by: MemoryLayout<Person>.stride).initialize(to: Person(age: 18, name: "小明"))
// 当前程序运行完成后 执行defer
defer {
// 这两个是成对出现的
p.deinitialize(count: 2)
p.deallocate()
}
2.3 内存指针的使用-内存绑定
Swift
提供了三种不同的API来绑定/重新绑定指针:
-
assumingMemoryBound(to:)
有些时候我们处理代码的过程中只有原生指针(没有报错指针类型),但此刻对于处理代码的的我们来说明确知道指针的类型,我们就可以使用assumingMemoryBound(to:)
来告诉编译器预期的类型。
(注意:这里只是让编译器绕过类型检查,并没有发生实际的类型转换)
func testPointer(_ p: UnsafePointer<Int>) {
print(p[0])
print(p[1])
}
// 这里的元祖是值类型,本质上这块内存空间中存放的就是Int类型的数据
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类型的数据
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:)
当我们在给外部函数传递参数时,不免会有一些数据类型上的差距,如果我们进行类型转换,必然要来会复制数据,这个时候就可以调用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文件中的属性和函数表
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
// 循环遍历属性
for i in 0..<fieldDescriptor!.numFields{
// FieldRecord 结构体由 3个 4字节组成,并且保持3 * 4 = 12字节对齐
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
// 函数的结构体
struct TargetMethodDescriptor {
var kind: UInt32
var offset: UInt32
}
// 获取方法的数量
if let methods = classDescriptor?.methods {
for i in 0..<methods {
// 获取v-table的的首地址
let VTableRelaticveAddress = typeDescOffset + 4 * 13 + mhHeaderPtr_IntRepresentation
// 获取当前函数的地址
let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
// 将 当前函数 的内存地址直接转换成指向 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
,发现没有正确获取到程序运行的基地址,但是在 Xcode 12
中不会出现这样的问题。
发现
_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
}
}
就是循环遍历 _dyld_get_image_header
中的元素判断是不是 mach-o
的执行地址。
二:内存管理
Swift
中使用自动引用计数(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
现在这一个 Person
实例已经有三个强引用了。
将其中两个变量赋值 nil
的方式断开两个强引用(包括最先的那个强引用),只留下一个强引用,Person
实例不会被销毁
p1 = nil
p2 = nil
只有当最后一个引用被断开时 ARC
才会销毁它
p3 = nil
// 打印结果
小明 is being deinitialized
2. 弱引用
弱引用不会对其引用的实例保持强引用,因而不会阻止 ARC
销毁被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 weak
关键字表明这是一个弱引用。
因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC
会在引用的实例被销毁后自动将其弱引用赋值为 nil
。并且因为弱引用需要在运行时允许被赋值为 nil
,所以它们一定是可选类型。
class Person {
var age: Int = 18
var name: String = "小明"
}
weak var t = Person()
进入汇编代码
我们可以看到这里的实质是调用了
swift_weakInit
函数,根据 Swift 源码的分析,其内部实现其实就是:一个对象在初始化的时候后是没有 SideTable
(散列表)的,当我们创建一个弱引用的时候,系统会创建一个 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>.
的实现。 -
InlineRefCountBits
和SideTableRefCountBits
共享当前模板类RefCountBitsT<bool>
-
InlineRefCounts
其实是一个uint64_t
可以当引用计数也可以当Side Table
的指针 -
SideTableRefCounts
是一种名为HeapObjectSideTableEntry
的结构体,里面也有RefCounts
成员,内部是SideTableRefCountBits
,其实就是原来的uint64_t
加上一个存储弱引用数的uint32_t
3. 无主引用
和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用在其他实例有相同或者更长的生命周期时使用。你可以在声明属性或者变量时,在前面加上关键字 unowned
表示这是一个无主引用。
但和弱引用不同,无主引用通常都被期望拥有值。所以,将值标记为无主引用不会将它变为可选类型,ARC
也不会将无主引用的值设置为 nil
。总之一句话就是,无主引用假定是永远有值的。
- 如果两个对象的生命周期完全和对方没关系(其中一方什么时候赋值为
nil
,对对方没有影响),使用weak
- 如果能确保:其中一个对象销毁,另一个对象也要跟着销毁,这时候可以(谨慎)使用
unowned
4. 闭包循环引用
闭包会一般默认捕获外部的变量
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
我们发现没有打印 Person deinit
,也就意味着 t
并没有被销毁,此时出现了循环引用。解决办法:就是使用捕获列表
func testARC() {
let t = Person()
t.testClosure = { [weak t] in
t?.age += 1
}
// t.testClosure = { [unowned t] in
// t.age += 1
// }
}
5. 捕获列表
默认情况下,闭包表达式从起周围的范围捕获常量和变量,并强引用这些值。可以使用捕获列表来显式控制如何在闭包中捕获值。
在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用 in
关键字。
创建闭包时,将初始化捕获列表中的条目。对于捕获列表中的每个条目,将常量初始化为在周围范围内具有相同名称的常量或变量的值。
var age = 0
var height = 0.0
let closure = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
closure()
// 打印结果
0
1.85
创建闭包时,内部作用域中的 age
会用外部作用域中的 age
的值进行初始化,但他们的值未以任何特殊方式连接。这意味着更改外部作用域中的 age
的值不会影响内部作用域中的 age
的值,也不会更改封闭内部的值,也不会影响封闭外的值。先比之下,只有一个名为 height
的变量-外部作用域中的 height
- 因此,在闭包内部或外部进行的更改在两个均可见。