简介
Swift 中的指针分为两类,typed pointer 指定数据类型指针,raw pointer 未指定数据类型的指针(原⽣指针)。这些都被定性为“Unsafe”(不安全的),常见的有以下4种类型:
UnsafePointer<Pointee>类似于const Pointee *。UnsafeMutablePointer<Pointee>类似于Pointee *。UnsafeRawPointer类似于const void *。UnsafeMutableRawPointer类似于void *。
指针为什么不安全
通过官方命名也可以看到,所有的指针有 unsafe官方已经在极力的提醒我们,指针是不安全的,通过以下几点来理解:
-
野指针
创建对象的时候需要分配内存,但是这块内存其实是有生命周期的,当我们使用指针来指向这块内存的时候,有可能该内存的引用计数为0,也就是被销毁了,那么指针指向的时候就是一个未定义的行为,形成野指针。
-
越界
系统在分配内存的时候是有一定大小的,比如数组的大小是5,如果通过指针访问了index为6的位置则会访问到未知的内存空间。
-
类型不同
系统分配的内存空间都是有一定类型的,指针的类型可能和内存空间值的类型不一致,这也不安全,比如一个
Int8类型的指针指向了一个Int类型的数据,就可能会精度缺失。
原生指针
在使用Swift指针时,内存管理需要手动管理,也就是创建的指针在最后需要调用deallocate。使用指针最终还是要指向我们创建的各种类型,所以先要了解内存布局方式的三个重要信息
- size 实际需要的大小
- stride 步长,连续存储多个元素指针间的间隔,也可以理解为系统最终分配的大小
- alignment 内存对齐的基数,最终分配的内存空间大小是它的倍数
原生指针的使用
我们创建一个可变的原生指针,8字节大小,8字节对齐。用 storeBytes 方法存值,load 方法取值,代码如下
// 创建一个可变的原生指针,8 个字节大小,8 字节对齐
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 8, alignment: 8)
// 存值
ptr.storeBytes(of: 100, as: Int.self)
// 取值
let value = ptr.load(as: Int.self)
print(value)
接下来我们创建一个 32 字节大小, 8 字节对齐的可变原生指针,如下:
我们将
i存入指针,需要在调用 storeBytes 方法之前调用 advanced 方法。advanced 传一个值,可以将值偏移指定的距离。需要注意的是 advanced 传的值,这个值是根据 stride,系统分配给结构体的内存大小。
因为存的时候是根据索引偏移了指定的距离存储值,所以取值的时候,也需要偏移指定的距离取值, fromByteOffset 参数指定需要偏移的距离。
泛型指针
相对原⽣指针来说,泛型指针就是指定当前指针已经绑定到了具体的类型。
在进⾏泛型指针访问的过程中,我们并不是使⽤ load 和 store ⽅法来进⾏存储操作。这⾥我们使⽤到当前泛型指针内置的变量 pointee 获取 UnsafePointer 的⽅式有两种。⼀种⽅式就是通过已有变量获取。可以用泛型指针获得指向某个变量的指针。
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 5)
//下标访问
ptr[0] = 1
ptr[1] = 2
//内存平移
p.advanced(by: 2).initialize(to: 3)
//销毁空间
p.deinitialize(count: 5)
p.deallocate()
//0x10070aae0: 0x0000000000000001 0x0000000000000002
//0x10070aaf0: 0x0000000000000003 0x0000000000000000
//0x10070ab00: 0x0000000000000000 0x0000000000000000
//0x10070ab10: 0x0000000000000000 0x0000000000000000
获取指向堆空间实例的指针
class Swagger {
}
var s = Swagger()
var ptr = withUnsafePointer(to: &s) { UnsafeRawPointer($0) }
var heapPtr = UnsafeRawPointer(bitPattern: ptr.load(as: UInt.self))
print(heapPtr ?? "")
内存绑定
Swift提供了三种不同的API来绑定/重新绑定指针
1. assumingMemoryBound(to:)
有些时候我们在处理代码的过程中,只有原始指针(没有保留指针类型),如果我们明确知道指针的类型,我们就可以使⽤ assumingMemoryBound(to:) 来告诉编译器预期的类型。注意:这⾥只是让编译器绕过类型检查,并没有发⽣实际类型的转换。
func testPointer(p: UnsafePointer<Int>) {
print(p[0])
print(p[1])
}
// 元组是值类型,里面存储 10 和 20,那本质上这块内存空间存储的就是 Int 类型的数据
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
// 先转换成原生指针,然后调用 assumingMemoryBound(to:) 方法,告诉编译器当前内存已经绑定过 Int 类型了,这个时候编译器不会检查。
testPointer(p: UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
2. bindMemory(to: capacity:)
⽤于更改内存绑定的类型,如果当前内存还没有类型绑定,则将⾸次绑定为该类型;否则重新绑定该类 型,并且内存中所有的值都会变成该类型。
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
testPointer(p: UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 2))
}
3. withMemoryRebound(to: capacity: body:)
当我们在给外部函数传递参数时,难免会有⼀些数据类型上的差距。如果我们进⾏类型转换,必然要来回复制数据;这个时候我们就可以使⽤ withMemoryRebound(to: capacity: body:) 来临时更改内存绑定类型。
func testPointer(_ p: UnsafePointer<Int8>){
print(p)
}
let uint8Ptr = UnsafePointer<UInt8>.init(bitPattern: 10)
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1, { (int8Ptr: UnsafePointer<Int8>) in
testPointer(int8Ptr)
})