Swift 中的指针操作
来源:互联网 发布:逆战刷永久武器软件 编辑:程序博客网 时间:2024/06/18 11:07
http://www.swiftyper.com/2017/01/15/unsafe-swift/
默认情况下,Swift 是内存安全的,这意味着它禁止我们直接操作内存,并且确保所有的变量在使用前都已经被正确地初始化了。但是,Swift 也提供了我们使用指针直接操作内存的方法,直接操作内存是很危险的行为,很容易就出现错误,因此官方将直接操作内存称为 “unsafe 特性”。
一旦我们开始直接操作内存,一切就得靠我们自己了,因为在这种情况下编译能给我们提供的帮助实在不多。正常情况下,我们在与 C 进行交互,或者我们需要挖掘 Swift 内部实现原理的时候会需要使用到这个特性。
Memory Layout
Swift 提供了 MemoryLayout
来检测特定类型的大小以及内存对齐大小:
12345678910111213141516171819
MemoryLayout<Int>.size// return 8 (on 64-bit)MemoryLayout<Int>.alignment// return 8 (on 64-bit)MemoryLayout<Int>.stride// return 8 (on 64-bit)MemoryLayout<Int16>.size// return 2MemoryLayout<Int16>.alignment// return 2MemoryLayout<Int16>.stride// return 2MemoryLayout<Bool>.size// return 1MemoryLayout<Bool>.alignment// return 1MemoryLayout<Bool>.stride// return 1MemoryLayout<Float>.size// return 4MemoryLayout<Float>.size// return 4MemoryLayout<Float>.alignment// return 4MemoryLayout<Double>.stride// return 8MemoryLayout<Double>.alignment// return 8MemoryLayout<Double>.stride// return 8
MemoryLayout<Type>
是一个用于在编译时计算出特定类型(Type)的 size, alignment 以及 stride 的泛型类型。返回的数值以字节为单位。例如 Int16
类型的大小为 2 个字节,内存对齐为 2 个字节以及当我们需要连续排列多个 Int16
类型时,每一个 Int16
所需要占用的大小(stride)为 2 个字节。所有基本类型的 stride 都与 size 是一致的。
接下来,看看结构体类型的 MemoryLayout:
1234567891011121314
struct EmptyStruct {}MemoryLayout<EmptyStruct>.size// returns 0MemoryLayout<EmptyStruct>.alignment // returns 1MemoryLayout<EmptyStruct>.stride// returns 1struct SampleStruct {let number: UInt32let flag: Bool}MemoryLayout<SampleStruct>.size// returns 5MemoryLayout<SampleStruct>.alignment// returns 4MemoryLayout<SampleStruct>.stride// returns 8
空结构体的大小为 0,内存对齐为 1, 表明它可以存在于任何一个内存地址上。有趣的是 stride 为 1,这是因为尽管结构为空,但是当我们使用它创建一个实例的时候,它也必须要有一个唯一的地址。
对于 SampleStruct
,它所占的大小为 5,但是 stride 为 8。这是因为编译需要为其填充空白的边界,使其符合它的 4 字节内存边界对齐。
再来看看类:
1234567891011121314
class EmptyClass {}MemoryLayout<EmptyClass>.size// returns 8 (on 64-bit)MemoryLayout<EmptyClass>.alignment// returns 8 (on 64-bit)MemoryLayout<EmptyClass>.stride// returns 8 (on 64-bit)class SampleClass {let number: Int64 = 0let flag: Bool = false}MemoryLayout<SampleClass>.size// returns 8 (on 64-bit)MemoryLayout<SampleClass>.aligment// returns 8 (on 64-bit)MemoryLayout<SampleClass>.stride// returns 8 (on 64-bit)
由于类都是引用类型,所以它所有的大小都是 8 字节。
关于 MemoryLayout 的更多详细信息可以参考 Mike Ash 的演讲。
指针
一个指针就是对一个内存地址的封装。在 Swift 当中直接操作指针的类型都有一个 “unsafe” 前缀,所以它的指针类型称为 UnsafePointer
。这个前缀似乎看起来很令人恼火,不过这是 Swift 在提醒你,你现在正在跨越雷池,编译器不会对这种操作进行检查,你需要对自己的代码承担全部的责任。
Swift 中包含了一打类型的指针类型,每个类型都有它们的作用和目的,使用适当的指针类型可以防止错误的发生并且更清晰地表达开发者的意图,防止未定义行为的产生。
Swift 的指针类型使用了很清晰的命名,我们可以通过名字知道这是一个什么类型的指针。可变或者不可变,原生(raw)或者有类型的,是否是缓冲(buffer)类型,这三种特性总共组合出了 8 种指针类型。
接下来的几个小节会详细介绍这几种指针类型。
使用原生(Raw)指针
在 Playground 中添加如下代码:
1234567891011121314151617181920212223242526272829
// 1let count = 2let stride = MemoryLayout<Int>.stridelet alignment = MemoryLayout<Int>.alignmentlet byteCount = stride * count// 2do {print("Raw pointers")// 3let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)// 4defer {pointer.deallocate(bytes: byteCount, alignedTo: alignment)}// 5pointer.storeBytes(of: 42, as: Int.self)pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)pointer.load(as: Int.self)pointer.advanced(by: stride).load(as: Int.self)// 6let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)for (index, byte) in bufferPointer.enumerated() {print("byte \(index): \(byte)")}}
在这个代码段中,我们使用了 Unsafe Swift 指针去存储和读取两个整型数值。接下来是对这段代码的解释:
- 声明了接下来都会用到的几个常量:
count
表示了我们要存储的整数的个数stride
表示了 Int 类型的 stridealignment
表示了 Int 类型的内存对齐byteCount
表示占用的全部字节数
- 使用
do
来增加一个作用域,让我们可以在接下的示例中复用作用域中的变量名 - 使用
UnsafeMutableRawPointer.allocate
方法来分配所需的字节数。我们使用了 UnsafeMutableRawPointer,它的名字表明这个指针可以用来读取和存储(改变)原生的字节。 - 使用 defer 来保证内存得到正确地释放。操作指针的时候,所有内存都需要我们手动进行管理。
storeBytes
和load
方法用于存储和读取字节。第二个整型数值的地址通过对 pointer 的地址前进 stride 来得到。因为指针类型是Strideable
的,我们也可以直接使用指针算术运算(pointer+stride).storeBytes(of: 6, as: Int.self)
。UnsafeRawBufferPointer
类型以一系列字节的形式来读取内存。这意味着我们可以这些字节进行迭代,对其使用下标,或者使用filter
,map
以及reduce
这些很酷的方法。缓冲类型指针使用了原生指针进行初始化。
使用类型指针
我们可以使用类型指针实现跟上面代码一样的功能,并且更简单:
1234567891011121314151617181920
do {print("Typed pointers")let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)pointer.initialize(to: 0, count: count)defer {pointer.deinitialize(count: count)pointer.deallocate(capacity: count)}pointer.pointee = 42pointer.advanced(by: 1).pointee = 6pointer.pointeepointer.advanced(by: 1).pointeelet bufferPointer = UnsafeBufferPointer(start: pointer, count: count)for (index, value) in bufferPointer.enumerated() {print("value \(index): \(value)")}}
注意到以下几点不同:
- 我们使用了 UnsafeMutablePointer.allocate 进行内存的分配。指定的泛型参数让 Swift 知道我们将会使用这个指针来存储和读取 Int 类型的值。
- 在使用类型指针前需要对其进行初始化,并在使用后销毁。这两个功能分别是使用 initialize 和 deinitialize 方法。
- 类型指针提供了
pointee
属性,它可以以类型安全的方式读取和存储值。 - 当需要指针前进的时候,我们只需要指定想要前进的个数。类型指针会自动根据它所指向的数值类型来计算 stride 值。同样的,我们可以直接对指针进行算术运算
(pointer + 1).pointee = 6
。 - 有类型的缓冲型指针也会直接操作数值,而非字节
将原生指针转换为类型指针
类型指针并不总是使用初始化得到的,它们可以从原生指针中转化而来。
在 Playground 中添加如下代码:
123456789101112131415161718192021222324
do {print("Converting raw pointers to typed pointers")let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)defer {rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)}let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)typedPointer.initialize(to: 0, count: count)defer {typedPointer.deinitialize(count: count)}typedPointer.pointee = 42typedPointer.advanced(by: 1).pointee = 6typedPointer.pointeetypedPointer.advanced(by: 1).pointeelet bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)for (index, value) in bufferPointer.enumerated() {print("value \(index): \(value)")}}
这段代码与上一段类似,除了它先创建了原生指针。我们通过将内存绑定(binding)到指定的类型上来创建类型指针。通过对内存的绑定,我们可以通过类型安全的方法来访问它。将我们手动创建类型指针的时候,系统其实自动帮我们进行了内存绑定。
获取一个实例的字节
很多时候我们需要从一个现存的实例里获取它的字节。这时可以使用 withUnsafeBytes(of:)
方法。
在 Playground 中添加如下代码:
1234567891011
do {print("Getting the bytes of an instance")var sampleStruct = SampleStruct(number: 25, flag: true)withUnsafeBytes(of: &sampleStruct) { bytes infor byte in bytes {print(byte)}}}
这段代码会打印出 SampleStruct
实例的原生字节。withUnsafeBytes(of:)
方法可以获取到UnsafeRawBufferPointer
并传入闭包中供我们使用。
withUnsafeBytes
同样适合用 Array
和 Data
的实例。
使用 Swift 操作指针的三大原则
当我们使用 Swift 操作指针的时候必须加倍小心,防止写出未定义行为的代码。下面是几个坏代码的示例。
不要从 withUnsafeBytes 中返回指针
123456789101112
// Rule #1do {print("1. Don't return the pointer from withUnsafeBytes!")var sampleStruct = SampleStruct(number: 25, flag: true)let bytes = withUnsafeBytes(of: &sampleStruct) { bytes inreturn bytes // strange bugs here we come ☠️☠️☠️}print("Horse is out of the barn!", bytes) /// undefined !!!}
绝对不要让指针逃出 withUnsafeBytes(of:)
的作用域范围。这样的代码会成为定时炸弹,你永远不知道它什么时候可以用,而什么时候会崩溃。
一次只绑定一种类型
12345678910111213141516171819202122
// Rule #2do {print("2. Only bind to one type at a time!")let count = 3let stride = MemoryLayout<Int16>.stridelet alignment = MemoryLayout<Int16>.alignmentlet byteCount = count * stridelet pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)// Breakin' the Law... Breakin' the Law (Undefined behavior)let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)// If you must, do it this way:typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {(boolPointer: UnsafeMutablePointer<Bool>) inprint(boolPointer.pointee) // See Rule #1, don't return the pointer}}
绝对不要让一个内存同时绑定两个不同的类型。如果你需要临时这么做,可以使用withMemoryRebound(to:capacity:)
来对内存进行重新绑定。并且,这条规则也表明了不要将一个基本类型(如 Int)重新绑定到一个自定义类型(如 class)上。不要做这种傻事。
不要操作超出范围的内存
12345678910111213141516
// Rule #3... waitdo {print("3. Don't walk off the end... whoops!")let count = 3let stride = MemoryLayout<Int16>.stridelet alignment = MemoryLayout<Int16>.alignmentlet byteCount = count * stridelet pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1????for byte in bufferPointer {print(byte) // pawing through memory like an animal}}
这是最糟糕的一种错误了,请再三检查你的代码,保证不要有这种情况出现。切记。
示例:随机数生成
随机数在很多地方都有重要的作用,从游戏到机器学习。macOS 提供了 arc4random
方法用于随机数生成。不幸的是,这个方法无法在 Linux 上使用。并且,arc4random
方法只提供了 UInt32
类型的随机数。事实上,/dev/urandom
这个设备文件中就提供了无限的随机数。
这一小节中,我们将使用指针读取这个文件,并产生完全类型安全的随机数。
创建一个新 Playground,命名为 RandomNumbers,并确保选择了 macOS
平台。
创建完成后,添加如下代码:
12345678910111213141516171819
import Foundationenum RandomSource {static let file = fopen("/dev/urandom", "r")!static let queue = DispatchQueue(label: "random")static func get(count: Int) -> [Int8] {let capacity = count + 1 // fgets adds null terminationvar data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)defer {data.deallocate(capacity: capacity)}queue.sync {fgets(data, Int32(capacity), file)}return Array(UnsafeMutableBufferPointer(start: data, count: count))}}
为了确保整个系统中只存在一个 file
变量,我们对其使用了 static
修饰符。系统会在我们的进程结束时关闭文件。因为我们有可能在多个线程中同时获取随机数,所以需要使用一个串行的 GCD 队列来进行保护。
get
函数是所有功能完成的地方。首先,我们根据传入的大小分配了必要的内存,注意这里需要 +1 是因为fets
函数总是以 \0
结束。接下来,我们就使用 fgets
函数从文件中读取数据,确保我们在串行队列中进行读取操作。最后,我们先将数据封装为一个 UnsafeMutableBufferPointer
,并将其转化为一个数组。
在 playground 的最后添加如下代码:
123456789101112131415161718192021
extension Integer {static var randomized: Self {let numbers = RandomSource.get(count: MemoryLayout<Self>.size)return numbers.withUnsafeBufferPointer { bufferPointer inreturn bufferPointer.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {return $0.pointee}}}}Int8.randomizedUInt8.randomizedInt16.randomizedUInt16.randomizedInt16.randomizedUInt32.randomizedInt64.randomizedUInt64.randomized
这里我们为 Integer 协议添加了一个静态属性,并为其提供了默认实现。我们首先获取了随机数,随后我们将获得字节数组重新绑定为所需要的类型,然后返回它的值。简单!
就这样,我们使用 unsafe Swift 实现了一个类型安全的随机器生成方法。
小结
在日常开发中,我们并不会接触到很多直接操作内存的情境。但是掌握它的操作,能让我们在碰到类似代码里更加从容。
参考资料
- Unsafe Swift: Using Pointers And Interacting With C
- Swift Evolution 0107: UnsafeRawPointer API
- Swift Evolution 0138: UnsafeRawBufferPointer API
- Interacting with C APIs
作者: 小锅
来源: http://www.swiftyper.com
链接: http://www.swiftyper.com/2017/01/15/unsafe-swift/
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可
- Swift 中的指针操作
- [Swift]Swift 中的指针使用
- Swift 中的指针使用
- Swift 中的指针使用
- Swift 中的指针使用
- swift中的指针
- Swift 中的指针使用
- swift中的文件操作
- 指针 与 swift 中的引用
- Swift中的as操作符
- Swift 2中的指针与Objective-C指针相互传递
- C语言中的指针操作
- Swift指针
- Swift中的条件操作和循环
- Swift 3.0中C语言指针类型在Swift编程语言中如何操作
- C++中的指针和->操作符
- [Swift]Swift中的!和?
- (八)swift 中UINavigationController 中的一些操作
- 中国(北方)大学生程序设计训练赛(第二周)(Problem G: Connected Components-并查集)
- mysql配置变量查看,执行线程查看
- 数据库
- gmapping算法学习=粒子滤波+卡尔曼滤波
- GraphicsMagick在Linux环境的安装【jpeg failed tests处理】
- Swift 中的指针操作
- CSS 自定义属性 — 基础篇
- hash
- windows2008新增硬盘无法创建分区,提示“由于管理员设置的策略 该磁盘处于脱机状态”
- FishC笔记—02 讲:用 Python 设计第一个游戏
- iWatch开发:WatchOS 消息推送教程
- 米斯特白帽培训讲义(v2)实战篇 捷达系统
- Java POI --- Java导入excel文档的相应列的数据到数据库的相应字段
- c/c++ 前置声明 -- typedef问题