「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
在 《不安全的Swift》一文中,我们介绍了,不安全的定义和Swift不安全缓冲区指针的使用,并介绍了Swift中,unsafe相关的API。在本文中,我们来探讨如何在Swift中安全的管理指针。C语言是未定义行为的源,Swift中也提供了相同底层级功能的API。安全管理指针意味着我们需要知道它们可能变得不安全的所有方式,我们可以将指针安全看作是一系列安全层级
Swift安全层级
一共分为4种安全层级,每往下一层,我们就需要确保编写的代码的正确性。
- 1,第一级是安全代码,Swift提供了强大的类型系统,提供了很多可能要通过指针获取的功能,完全不使用指针是最安全的。
- 2,第二级是不安全API,提供了
类型指针供我们使用,来实现高效互用性,通常以Unsafe作为函数的前缀 - 3,第三级是
原始指针,如果你需要将 某些原始内存用作一连串的字节, Swift 提供了UnsafeRawPointer选项 利用原始内存加载和存储值,此时,你有责任知道此时内存中的数据类型。 - 4,第四级,Swift提供了
内存绑定的API,可以将 内存绑定到类型上,只有当你使用这些最底层级的API是,就需要承担管理指针类型安全的所有责任。
类型指针
UnsafePointer<T>是一个类型指针,它表示存储在内存里的值类型,该内存位置只能保存该类型的值。作为一个类型指针,只从内存里读取该内存的值。UnsafeMutablePointer<T>能够读取或存入该类型的值。
在 Swift里,访问一个类型参数和其内存绑定类型不一致的指针,会产生未定义的行为
为避免这个问题,在编译期,
Swift类型系统会执行额外的运行检查。
指向变量的类型指针
声明一个Int类型的变量,并请求一个指向变量存储的指针withUnsafePointer,这样就会得到和声明一致的pointer
指向数组的类型指针
声明一个Array,使用withUnsafeBufferPointer,就会得到指向数组存储的指针。
类型安全的静态内存分配
struct Collage {
var name: String
}
var c = Collage(name: "A")
let tPtr = UnsafeMutablePointer<Collage>.allocate(capacity: 5) // 1
print(tPtr)
tPtr.initialize(repeating: c, count: 5) // 2
tPtr.assign(repeating: c, count: 5) // 3
tPtr.deinitialize(count: 5) // 4
tPtr.deallocate() //4
- 1, 使用
UnsafeMutablePointer静态分配内存,并将内存绑定至它的类型参数。 - 2,使用分配给予的类型指针,针对正确的类型,进行
初始化内存。 - 3,在
初始化状态下,内存可以被重新分配,重新分配会暗地里取消初始先前的内存内值 并将内存重新初始成相同类型的新值。 - 4,使用同一类型指针取消初始内存,此时内存依然会绑定至相同类型,可以安全的解除分配。
在这里,
你只需要负责管理内存的初始化状态,swift来确保类型安全。
复合类型
此处,我们拥有一个包含
MyStruct值的内存块,我们可以获取一个指向结构体外层的指针或者指向其属性的指针,而这两种指针都是有效的,可以访问其中任意一个,无需更改内存绑定的类型。这依然遵照了相同的指针安全基本规则 当内存绑定至一个复合类型时 它也会有效地和该类型的成员绑定 因为它们都存在于内存的布局里。
对于类型指针,我们不需要管理内存绑定的数据类型,但我们需要管理其声明周期和内存边界。
类型指针可以让我们直接访问内存,但仅限于类型安全的范围内,不能拥有两个指向同一内存却拥有不同类型指针。因此 如果你的目标是 将内存字节重新诠释成不同类型 你就需要使用一个低层级的 API。
原始指针
UnsafeRawPointer可以从一连串字节中读取数据,不需要指定它们可能代表的值类型
存储和读取内存
let ptr = UnsafeMutablePointer<Int64>.allocate(capacity: 1) // 1
ptr.initialize(repeating: 4294967297, count: 1)
ptr.assign(repeating: 4294967297, count: 1)
print(ptr.pointee)
var rPtr = UnsafeRawPointer(ptr) // 2
let value = rPtr.load(as: UInt32.self) // 3
print(value)
var mPtr = UnsafeMutableRawPointer(ptr) // 4
mPtr.storeBytes(of: 10, as: UInt32.self)
print(mPtr.load(as: UInt32.self))
- 1,使用
类型指针,将内存初始化为Int64类型的内存块,并写入4294967297 == UInt32.max + 2 - 2,将
类型指针转变为原始指针。 - 3,从内存空间中以
UInt32的形式,读取内存值。 - 4,将
类型指针转化为可变的原始指针,对内存进行操作。
我们来看下输出情况
4294967297
1
10
我们可以看到,当我们使用UInt32类型读取Int64类型的内存值时,此时,数据出现了错误。因为只读取了前32位的数据,此时,保证数据的安全性,就变成了你的责任。
类型指针可以变为原始指针,原始指针不可以变为类型指针
指向变量的原始指针
使用withUnsafeBytes可以获得指向变量的原始指针UnsafeRawBufferPointer
var i: Int32 = 10
withUnsafeBytes(of: i) { (ptr: UnsafeRawBufferPointer) in
print(ptr.count)
print(MemoryLayout<Int32>.size) // MemoryLayout<Int32>.size == ptr.count
print("0:\(ptr[0]), 1: \(ptr[1]), 2: \(ptr[2]), 3: \(ptr[3])")
}
在这里,内存缓冲区计数就是变量类型字节的大小,集合索引是字节偏移量,读取索引处的元素,会返回UInt8的值,就是依次读取1个字节。ptr[1]就是从第一个字节偏移量中读取1个字节大小的值,
我们来看下输出情况
4
4
0:255, 1: 255, 2: 255, 3: 127
此时连续的32位存储空间的数据如下图所示
使用withUnsafeMutableBytes可以得到可变的原始指针UnsafeMutableRawBufferPointer,可以修改特定字节偏移量处的内存值。
var j: Int32 = Int32.max
withUnsafeMutableBytes(of: &j) { (ptr: UnsafeMutableRawBufferPointer) in
ptr.storeBytes(of: 10, toByteOffset: 1, as: UInt8.self)
print("0:\(ptr[0]), 1: \(ptr[1]), 2: \(ptr[2]), 3: \(ptr[3])")
}
此时,已经将集合索引为1的地方的内存值,修改为10
输出结果如下
0:255, 1: 10, 2: 255, 3: 127
指向数组的原始指针
withUnsafeBytes这个方法能够得到指向原始内存存储字节的指针。
var array:[Int] = [1, 2, 3]
array.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
print(bytes.count)
print(MemoryLayout<Int>.stride * array.count) //bytes.count == MemoryLayout<Int>.stride * array.count
}
此时获得的缓冲区的大小等于数据类型的步幅 * 集合的数量
指向Data的原始指针
import Foundation
func readUInt32(data: Data) -> UInt32 {
data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
buffer.load(fromByteOffset: 4, as: UInt32.self)
}
}
let data = Data(Array<UInt8>([0, 0, 0, 0, 1, 0, 0, 0]))
print(readUInt32(data: data))
// 输出结果
1
我们可以使用该方法,使用二进制数据,在特定的字节偏移量中,读取特定的元素类型。
指向静态分配的原始指针
func rawAllocate<T>(t: T, numValues: Int) -> UnsafeMutablePointer<T> {
let rawPtr = UnsafeMutableRawPointer.allocate(
byteCount: MemoryLayout<T>.stride * numValues,
alignment: MemoryLayout<T>.alignment) // 1
let tPtr = rawPtr.initializeMemory(as: T.self, repeating: t, count: numValues) // 2
// Must use the typed pointer ‘tPtr’ to deinitialize.
return tPtr
}
var ptr = rawAllocate(t: Int.self, numValues: 10)
- 1, 使用
UnsafeMutableRawPointer.allocate在内存上申请一段连续的内存空间,并获得指向该块内存的指针。这时候,需要我们自己计算字节内存的大小和内存对齐的方式,经过原始分配后内存状态既不初始化 也不绑定至类型。 - 2,指定内存中存储的值类型,将
内存绑定至该类型,并返回类型指针,如果内存已经是初始化状态,必须使用返回的类型指针,来deinitialize该处内存。如果没有初始话,那么可以使用原始指针rawPtr,来销毁内存。
使用类型指针进行内存分配,更加安全快捷,但有有些情况不得不使用原始指针
连续存储不同类型的数据
当我们在一块连续的内存空间,存储不同类型的数据时,使用原始指针就变成了一个很好的选择。
有以下场景:在内存的起始部分,我们需要存入Header类型的数据,紧接着存储Int32类型的数据
这个时候,我们就需要通过原始指针偏移一个Header大小的空间,然后依次读取4字节来读取数据。
func contiguousAllocate<Header>(header: Header, numValues: Int) -> (UnsafeMutablePointer<Header>, UnsafeMutablePointer<Int32>) {
let offset = MemoryLayout<Header>.stride
let byteCount = offset + MemoryLayout<Int32>.stride * numValues
assert(MemoryLayout<Header>.alignment >= MemoryLayout<Int32>.alignment)
let bufferPtr = UnsafeMutableRawPointer.allocate(
byteCount: byteCount, alignment: MemoryLayout<Header>.alignment)
let headerPtr = bufferPtr.initializeMemory(as: Header.self, repeating: header, count: 1)
let elementPtr = (bufferPtr + offset).initializeMemory(as: Int32.self, repeating: 0, count: numValues)
return (headerPtr, elementPtr)
}
应用案例
decoding bytes buffers
当你拥有外部生成的字节缓冲区 并且想将这些字节解码成 Swift 类型时 你就会想要使用原始指针
import Foundation
func readUInt32(data: Data, byteOffset: Int) -> UInt32 {
data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
buffer.load(fromByteOffset: byteOffset, as: UInt32.self)
}
}
let data = Data(Array<UInt8>([0, 0, 0, 0, 1, 0, 0, 0]))
print(readUInt32(data: data, byteOffset: 0)) // 0
print(readUInt32(data: data, byteOffset: 4)) // 1
使用原始指针的时候,我们需要额外注意此时的内存的空间布局
内存绑定
swift提供了3个内存绑定的API
assumingMemoryBound(to:)bindMemory(to:capacity:)withMemoryRebound(to:capacity)在使用内存绑定时,我们要遵守一个规则:类型指针的访问需和内存绑定类型一致。
assumingMemoryBound
来看下如下示例:
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ }
struct RawContainer { // 1
var rawPtr: UnsafeRawPointer
var pointsToInt: Bool
}
func testContainer(numValues: Int) {
let intPtr = UnsafeMutablePointer<Int>.allocate(capacity: numValues)
let rc = RawContainer(rawPtr: intPtr, pointsToInt: true)
// ...
if rc.pointsToInt {
takesIntPointer(rc.rawPtr.assumingMemoryBound(to: Int.self)) // 2
}
}
- 1,保留原始内存的容器。
- 2,
assumingMemoryBound告诉编译器,假设内存绑定的是Int类型的数据。只有当你确定内存已经被绑定为你想要的类型的时候,才可以使用assumingMemoryBound。
C语言回调
在处理C语言API的结果回调时,此时,我们也需要使用assumingMemoryBound函数。
// Use assumingMemoryBound to recover a pointer type from a (void *) C callback.
/*
func pthread_create(_ thread: UnsafeMutablePointer<pthread_t?>!,
_ attr: UnsafePointer<pthread_attr_t>?,
_ start_routine: (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer?,
_ arg: UnsafeMutableRawPointer?) -> Int32
*/
import Darwin
struct ThreadContext { /* elided */ }
func testPthreadCreate() {
let contextPtr = UnsafeMutablePointer<ThreadContext>.allocate(capacity: 1)
contextPtr.initialize(to: ThreadContext())
var pthread: pthread_t?
let result = pthread_create(
&pthread, nil,
{ (ptr: UnsafeMutableRawPointer) in
let contextPtr = ptr.assumingMemoryBound(to: ThreadContext.self)
// ... The rest of the thread start routine
return nil
},
contextPtr)
}
类型指针不一致
有时候,我们有一个类型指针,但类型指针的类型不一致
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ }
func testPointingToTuple() {
let tuple = (0, 1, 2)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int, Int)>) in
takesIntPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
}
将 (Int, Int, Int)型的类型指针,转化为原始指针,然后,使用assumingMemoryBound将其变化为类型为Int的 类型指针
结构体属性
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ }
struct MyStruct {
var status: Bool
var value: Int
}
func testPointingToStructProperty() {
let myStruct = MyStruct(status: true, value: 0)
withUnsafePointer(to: myStruct) { (ptr: UnsafePointer<MyStruct>) in
let rawValuePtr =
(UnsafeRawPointer(ptr) + MemoryLayout<MyStruct>.offset(of: \MyStruct.value)!)
takesIntPointer(rawValuePtr.assumingMemoryBound(to: Int.self))
} // 1
}
- 1,通过
withUnsafePointerAPI得到myStruct的内存地址,然后通过MemoryLayout获得value属性在结构体中的偏移量,从而获得value属性的真实内存地址。
上述操作等价于下面的操作,可以使用inout关键字,编译器会将它转换成函数所声明的指针类型。
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ }
struct MyStruct {
var status: Bool
var value: Int
}
func testPointingToStructProperty() {
let myStruct = MyStruct(status: true, value: 0)
takesIntPointer(&myStruct.value)
}
assumingMemoryBound会让编译器对内存的绑定类型进行一个不经检查的假设。
bindMemory
bindMemory可以更改内存的绑定类型。如果之前没有绑定任何类型,那么就会进行首次绑定,如果已经绑定至一个类型,那么它会重新绑定该类型。
func testBindMemory() {
let uint16Ptr = UnsafeMutablePointer<UInt16>.allocate(capacity: 2) // 1
uint16Ptr.initialize(repeating: 0, count: 2)
let int32Ptr = UnsafeMutableRawPointer(uint16Ptr).bindMemory(to: Int32.self, capacity: 1) // 2
// Accessing uint16Ptr is now undefined
int32Ptr.deallocate()
}
- 1,开辟2个
UInt16大小的空间,此时,内存绑定的类型是UInt16。 - 2,
bindMemory将该段内存空间重新绑定为Int32类型,此时在访问uint16Ptr,这个指针就是未定义的。
更改内存绑定类型,并不会从物理上调整内存,但我们应该把它想像成更改内存状态的属性
withMemoryRebound
func takesUInt8Pointer(_: UnsafePointer<UInt8>) { /* elided */ }
func testWithMemoryRebound(int8Ptr: UnsafePointer<Int8>, count: Int) {
int8Ptr.withMemoryRebound(to: UInt8.self, capacity: count) {
(uint8Ptr: UnsafePointer<UInt8>) in
// int8Ptr cannot be used within this closure
takesUInt8Pointer(uint8Ptr)
}
// uint8Ptr cannot be used outside this closure
}
withMemoryRebound在必要时暂时性的绑定内存,相对来说,比bindMemory安全一些。
bingMemory API总结
- 1,
assumingMemoryBound(to:):需要提前知道内存绑定的数据类型。 - 2,
bindMemory(to:capacity:):可以改变内存的绑定的数据类型状态。 - 3,
withMemoryRebound: 暂时性的改变内存绑定的状态。
内存绑定的常见错误
从内存中读取一个不同类型,使用bindMemory会更改内存状态,使其他指向该区域的指针失效。使用load(as:)是一个比较好的选择。
原始数据
withUnsafeBytes能够让我们直接访问变量,数组或者二进制数据的原始缓冲区。
struct UnsafeBufferView<Element>: RandomAccessCollection {
let rawBytes: UnsafeRawBufferPointer
let count: Int
init(reinterpret rawBytes: UnsafeRawBufferPointer, as: Element.Type) {
self.rawBytes = rawBytes
self.count = rawBytes.count / MemoryLayout<Element>.stride
precondition(self.count * MemoryLayout<Element>.stride == rawBytes.count)
precondition(Int(bitPattern: rawBytes.baseAddress).isMultiple(of: MemoryLayout<Element>.alignment))
}
var startIndex: Int { 0 }
var endIndex: Int { count }
subscript(index: Int) -> Element {
rawBytes.load(fromByteOffset: index * MemoryLayout<Element>.stride, as: Element.self)
}
}
func testBufferView() {
let array = [0,1,2,3]
array.withUnsafeBytes {
let view = UnsafeBufferView(reinterpret: $0, as: UInt.self)
for val in view {
print(val)
}
}
}
总结
以上就是Swift中,关于指针的所有内容了,在Swift代码中,这些也都是非常有用的。比如,我们可能需从字节流里解码值,或者需要自己实现诸如Set、Dictionary等等的数据容器。
如果觉得有收获请按如下方式给个
爱心三连:👍:点个赞鼓励一下。🌟:收藏文章,方便回看哦!。💬:评论交流,互相进步!。