iOS-Swift中的各种指针和使用

这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

前言

Objective-C和C语言经常需要使用到指针。Swift中的数据类型由于良好的设计,使其可以和基于指针的C语言API无缝混用。但是语法上有很大的差别。

默认情况下Swift是内存安全的,苹果官方并不鼓励直接去操作内存。但是,Swift中也提供了使用指针操作内存的方法,直接操作内存是很危险的行为,很容易就出现错误,因此官方将直接操作内存称为 unsafe 特性。

为什么说指针不安全?

  • ⽐如我们在创建⼀个对象的时候,是需要在堆分配内存空间的。当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,变成了野指针
  • ⽐如我们创建⼀个⼤⼩为20的数组,这个时候我们通过指针访问到 index = 25 的位置,这个时候是不是就越界了,访问了⼀个未知的内存空间;
  • 指针类型与内存的值类型不⼀致,也是不安全的;

在操作指针之前需要先了解 MemoryLayoutsize、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 种类型的指针,包含下面这些:

截屏2022-01-18 上午9.27.25.png

  • 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)")
}
复制代码

截屏2022-01-18 上午10.51.05.png

记得使用后可以在 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
}
复制代码

截屏2022-01-18 上午11.24.08.png

  • 直接分配内存
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:) 来告诉编译器预期的类型。 (注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换)

举个例子:

截屏2022-01-18 上午11.50.21.png

编译器就会检查出来当前指针存储的值类型不一致,无法转换

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))
    }
}
复制代码

按着这样写下来,就可以获取到类名和属性的名称。

参考文章:juejin.cn/post/705421…

猜你喜欢

转载自juejin.im/post/7054714518424780831