「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战。」
前言
Objective-C和C语言经常需要使用到指针。Swift中的数据类型由于良好的设计,使其可以和基于指针的C语言API无缝混用。但是语法上有很大的差别。
默认情况下Swift是内存安全
的,苹果官方并不鼓励直接去操作内存。但是,Swift中也提供了使用指针操作内存的方法,直接操作内存是很危险的行为,很容易就出现错误,因此官方将直接操作内存称为 unsafe
特性。
为什么说指针不安全?
- ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,变成了
野指针
; - ⽐如我们创建⼀个⼤⼩为20的数组,这个时候我们通过指针访问到 index = 25 的位置,这个时候是不是就
越界
了,访问了⼀个未知的内存空间; - 指针类型与内存的值
类型
不⼀致,也是不安全的;
在操作指针之前需要先了解 MemoryLayout
的 size、stride、alignment
这几个属性。
MemoryLayout 的介绍
获取特定类型占用内存的大小,单位为字节。
// 内存中占用的空间
MemoryLayout<T>.size
复制代码
// 类型T实际分配的内存大小(为了提高性能,会遵循内存对齐原则)
MemoryLayout<T>.stride
复制代码
// 内存对齐的基数(为了提高性能,任何的对象都会先进行内存对齐再使用)
MemoryLayout<T>.alignment
复制代码
比如下面获取对应的这三个大小分别是多少:
struct Point {
let x: Double
let y: Double
let isFilled: Bool
}
print(MemoryLayout<Point>.size)//17
print(MemoryLayout<Point>.stride)//24
print(MemoryLayout<Point>.alignment)//8
复制代码
size大小是17字节,因为x 和 y 是 Double 类型,占用 8 字节空间,isFilled 是 Bool 类型,占用 1 字节的空间
stride 大小是24,因为遵循8字节内存对齐的原则
alignment 大小是8,因为这个结构体中占用最大空间的 Double 类型数据内存大小是 8
所以在指针移动的过程中,一次移动一个stride
(步长)的大小,并且 stride 的大小是 alignment 的整数倍。
指针的讲解和使用
一个指针就是对一个内存地址的封装。由于Swift 语言认为操作内存是不安全的,所以在指针前面都加了 unsafe
前缀,指针类型为 UnsafePointer
。
Swift 的指针类型使用了很清晰的命名,通过查找发现了一共 8
种类型的指针,包含下面这些:
- Pointer Name:不可变/可变,原生(raw)/有类型、是否是缓冲类型(buffer)
- unsafe:不安全的
- Strideable:指针可使用 advanced 函数移动
- Write Access:可写入
- Collection:像一个容器,可添加数据
- Typed:是否需要指定类型(泛型)
UnsafePointer 的使用
举个例子:开辟一块内存空间,存储4个Int类型的数据。
由于 UnsafePointer 不能修改指针指向的内容,所以需要选用 UnsafeMutableRawPointer
//获取步长大小
let stride = MemoryLayout<Int>.stride
//开辟32字节大小的空间,8字节内存对齐
let ptr = UnsafeMutableRawPointer.allocate(byteCount: 4*stride, alignment: 8)
for i in 1..<5 {
ptr.advanced(by: i*stride).storeBytes(of: i, as: Int.self)
}
for i in 1..<5 {
let value = ptr.load(fromByteOffset: i*stride, as: Int.self)
print("index\(i)=value:\(value)")
}
复制代码
记得使用后可以在 defer 里面释放申请的内存空间,defer 会在函数返回前调用,需要在面调用 deallocate
。
上面用到的代码说明:
- allocate:方法来分配所需的字节数
- byteCount:占用的全部字节数
- alignment:表示类型的内存对其
- advanced:移动指针地址
- storeBytes:存储字节
- load:读取字节
- fromByteOffset:偏移指针地址
泛型指针的使⽤
泛型指针相⽐较原⽣指针来说,其实就是指定当前指针已经绑定到了具体的类型。同样,还是通过⼀个例⼦来解释⼀下。
在进⾏泛型指针访问的过程中,我们并不是使⽤ load 和 store
⽅法来进⾏存储操作。这⾥我们使⽤到当前泛型指针内置的变量 pointee
,它可以以类型安全的方式读取和存储值。
获取 UnsafePointer 的⽅式有两种
- 通过已有变量获取
var a = 20
//通过withUnsafePointer访问当前变量的地址
withUnsafePointer(to: &a) { ptr in
print(ptr)
}
//想要修改当前a的值,可以返回当前的修改,不能直接修改ptr.pointee的值
a = withUnsafePointer(to: &a) {ptr in
ptr.pointee + 20
}
withUnsafePointer(to: &a) { ptr in
print(ptr)
}
var b = 10
//通过withUnsafeMutablePointer可以直接修改值
withUnsafeMutablePointer(to: &b) { ptr in
ptr.pointee += 10
}
复制代码
- 直接分配内存
var a = 10
//分配一块Int类型的内存空间
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
//初始化分配的内存空间
ptr.initialize(to: a)
//访问当前内存空间的值
print(ptr.pointee)
//在使用完之后回收内存
defer {
ptr.deinitialize(count: 1)
ptr.deallocate()
}
复制代码
内存绑定
Swift 提供了三种不同的 API 来绑定/重新绑定指针:
1. assumingMemoryBound(to: )
有些时候我们处理代码的过程中,只有原始指针(没有保留指针类型),但此刻对于处理代码的我们来 说明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型
。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)
举个例子:
编译器就会检查出来当前指针存储的值类型不一致,无法转换
func testPointer(_ ptr: UnsafePointer<Int>) {
print(ptr[0])
print(ptr[1])
}
//元组是值类型,本质上这块内存空间存放的额就是Int类型的数据
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
//先转成原生指针,然后告诉编译器当前内存已经绑定Int,这个时候编译器不在会去检查
testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
复制代码
这样相当于绕过了编译器的检查,左右不会报错了。
2. bindMemory(to: capacity:)
⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类型,并且内存中所有的值都会变成该类型。
func testPointer(_ ptr: UnsafePointer<Int>) {
print(ptr[0])
print(ptr[1])
}
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 2))
}
复制代码
3. withMemoryRebound(to: capacity: body:)
当我们在给外部函数传递参数时,不免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:) 来临时更改内存绑定类型。
func testPointer(_ ptr: UnsafePointer<Int8>){
print(ptr)
}
let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 20)
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
testPointer(int8Ptr)
})
复制代码
UnsafeBufferPointer 的使用
UnsafeBufferPointer 指代一系列连续的内存。我们可以使用这个指针作为一个序列的指针,并通过指针直接访问元素。
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: 2)
pointer.pointee = 42
pointer.advanced(by: 1).pointee = 6
let bufferPointer = UnsafeBufferPointer(start: pointer, count: 2)
for (index, value) in bufferPointer.enumerated() {
print("value \(index): \(value)")
}
复制代码
打印结果 :
value 0: 42
value 1: 6
UnsafeMutableBufferPointer 的使用
可变的系列指针。UnsafeMutableBufferPointer
拥有对指向序列修改的能力。
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: 1)
let usmbp = UnsafeMutableBufferPointer<Int>.init(start: usmp, count: 5) // 拓展为5各元素的大小
usmbp[0] = 120
usmbp[1] = 130 //进行修改 其他未修改的内容将产生随机值
usmbp.forEach { (a) in
print(a)
}
print(usmbp.count)
复制代码
如果一个序列被初始化之后,没有给每一个元素赋值的话,这些元素的值都将出现随机值。
UnsafeRawBufferPointer 与 UnsafeMutableRawBufferPointer
UnsafeRawBufferPointer和UnsafeMutableRawBufferPointer 指代的是一系列的没有被绑定类型的内存区域
。我们可以理解成他们实际上就是一些数组,再绑定内存之前,其中包含的元素则是每一个字节。
let pointer = UnsafeMutableRawBufferPointer.allocate(byteCount: 3, alignment: MemoryLayout<Int>.alignment)
pointer.copyBytes(from: [1, 2, 3])
pointer.forEach {
print($0) // 1, 2, 3
}
复制代码
所以通过原始缓冲区从内存中读取是一种无类型操作, UnsafeMutableRawBufferPointer 实例可以写入内存, UnsafeRawBufferPointer 实例不可以。如果要类型化,必须将内存绑定到一个类型上
。
指针之间的转换
unsafeBitCast
函数是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据,类似于 C++ 中的 reinterpret_cast
。
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
ptr.assumingMemoryBound(to: Int.self).pointee = 20
(ptr + 8).assumingMemoryBound(to: Double.self).pointee = 10.0
print(unsafeBitCast(ptr, to: UnsafePointer<Int>.self).pointee) // 20
print(unsafeBitCast(ptr + 8, to: UnsafePointer<Double>.self).pointee) // 10.0
ptr.deallocate()
复制代码
上面这段代码,把 Int 类型的 20 和 Double 类型的 10.0 分别存储到 ptr 指针的内存中。在取值的时候,我们可以通过 unsafeBitCast
强制转换,分别取出它们的值。
指针加强练习
在前面所学的方法和属性中,我们通过源码 + 汇编分析
在 Mach-O
文件中找到了属性相关的信息。接下来我们通过指针来获取 Mach-O 文件中属性相关的信息。
这里是整理出来的类的底层结构:
class Person {
var age: Int = 18
var name: String = "Candy"
}
//类的描述信息
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
//V-Table
}
//属性信息
struct FieldDescriptor {
var MangledTypeName: UInt32
var Superclass: UInt32
var Kind: UInt16
var FieldRecordSize: UInt16
var NumFields: UInt32
var FieldRecords: FieldRecord
}
//某个属性信息
struct FieldRecord {
var Flags: UInt32
var MangledTypeName: UInt32
var FieldName: UInt32
}
复制代码
通过指针获取 Mach-O 文件的属性信息
这里对应的操作都是对 Mach-O 文件的读取,在写的过程中可以一边打印一遍对照 Mach-O 的的信息验证是否读取正确
// 1. 获取 __swift5_types 中正确的内存地址
var size: UInt = 0
// __swift5_types
let types_ptr = getsectdata("__TEXT", "__swift5_types", &size)
print(types_ptr)
// 获取 Mach-O 文件中 __LINKEDIT 的信息
var segment_command_linkedit = getsegbyname("__LINKEDIT")
// 获取该段的文件内存的地址
let vmaddr = segment_command_linkedit?.pointee.vmaddr
// 获取该段的文件偏移量
let fileoff = segment_command_linkedit?.pointee.fileoff
// 计算出链接的基地址(也就是虚拟内存的基地址)
var link_base_address = (vmaddr ?? 0) - (fileoff ?? 0)
// 前面拿到的 __swift5_types 的内存地址,是加了虚拟内存的基地址的。
// 所以要拿到 Swift 类的信息正确的内存地址,需要用 __swift5_types 的内存地址减去虚拟内存的基地址
var offset: UInt64 = 0
if let unwrapped_ptr = types_ptr {
// 把 types_ptr 转换成整型,进行计算
let types_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: unwrapped_ptr)))
offset = types_int_representation - link_base_address
print("offset: ", offset)
}
// 2. 获取 __swift5_types 中那四个字节的内容
// 获取当前程序运行的基地址
var app_base_address = _dyld_get_image_header(0)
print(app_base_address)
// 把 app_base_address 转换成整型,进行计算
let app_base_address_int_representation = UInt64(bitPattern: Int64(Int(bitPattern: app_base_address)))
// 计算出 __swift5_types 中四个字节在程序内存中存放的地址
var data_load_address = app_base_address_int_representation + offset
// 接下来需要拿到这四个字节指向的内容
// 将 data_load_address 转成指针类型
let data_load_address_ptr = withUnsafePointer(to: data_load_address) { $0 }
let data_load_content = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: data_load_address) ?? 0)?.pointee
print("data_load_content: ",data_load_content)
// 3. 获取 Description 的地址
// 获取 Description 在 Mach-O 文件中的偏移量
let description_offset = offset + UInt64(data_load_content ?? 0) - link_base_address
print("description_offset: ", description_offset)
// 获取 Description 在内存中的指针地址
let description_address = description_offset + app_base_address_int_representation
// 将 Description 的指针地址指向 TargetClassDescriptor
let class_description = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: description_address) ?? 0)?.pointee
print("class_description: ", class_description)
// 4. 获取属性信息 - FieldDescriptor
// 16 为 FieldDescriptor 结构体前面四个成员变量的大小,4 个 UInt32 ,所以为 16
let fieldDescriptor_address_int_representation = description_offset + 16 + app_base_address_int_representation
print("fieldDescriptor_address_int_representation: ", fieldDescriptor_address_int_representation)
// 将 fieldDescriptor_address_int_representation 转成指针地址,这里拿到的地址的值为 fieldDescriptor 的偏移信息
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldDescriptor_address_int_representation) ?? 0)?.pointee
print("fieldDescriptorOffset: ", fieldDescriptorOffset)
// fieldDescriptor 的真正的内存地址 = fieldDescriptor_address_int_representation + 偏移信息
let fieldDescriptorAddress = fieldDescriptor_address_int_representation + UInt64(fieldDescriptorOffset!)
print("fieldDescriptorAddress: ", fieldDescriptorAddress)
//将 fieldDescriptor 内存地址转成 FieldDescriptor
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee
print("fieldDescriptor: ", fieldDescriptor)
//遍历属性信息
for i in 0..<fieldDescriptor!.NumFields {
let a = MemoryLayout<FieldRecord>.stride
let stride: UInt64 = UInt64(i * UInt32(a))
let fieldRecordAddress = fieldDescriptorAddress + 16 + stride
print(fieldRecordRelactiveAddress)
//将指针绑定到FieldRecord结构体
//let fieldRecord = UnsafePointer<FieldRecord>.init(bitPattern: Int(exactly: fieldRecordAddress) ?? 0)?.pointee
//print(fieldRecord)
let fieldNameRelactiveAddress = (fieldRecordAddress + 8 - link_base_address) + app_base_address_int_representation
let offset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
//print(offset)
//获取属性name的真实内存地址
let fieldNameAddress = fieldNameRelactiveAddress + UInt64(offset!) - link_base_address
//转换成字符串显示出来
if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
print(String(cString: cChar))
}
}
复制代码
按着这样写下来,就可以获取到类名和属性的名称。