Swift中的指针操作详解

前言

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

默认情况下,Swift 是内存安全的,这意味着它禁止我们直接操作内存,并且确保所有的变量在使用前都已经被正确地初始化了。但是,Swift 也提供了我们使用指针直接操作内存的方法,直接操作内存是很危险的行为,很容易就出现错误,因此官方将直接操作内存称为 “unsafe 特性”。

一旦我们开始直接操作内存,一切就得靠我们自己了,因为在这种情况下编译能给我们提供的帮助实在不多。正常情况下,我们在与 C 进行交互,或者我们需要挖掘 Swift 内部实现原理的时候会需要使用到这个特性。

Memory Layout

Swift 提供了 MemoryLayout 来检测特定类型的大小以及内存对齐大小:

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 2
MemoryLayout<Int16>.alignment // return 2
MemoryLayout<Int16>.stride // return 2
MemoryLayout<Bool>.size // return 2
MemoryLayout<Bool>.alignment // return 2
MemoryLayout<Bool>.stride // return 2
MemoryLayout<Float>.size // return 4
MemoryLayout<Float>.size // return 4
MemoryLayout<Float>.alignment // return 4
MemoryLayout<Double>.stride // return 8
MemoryLayout<Double>.alignment // return 8
MemoryLayout<Double>.stride // return 8

MemoryLayout<Type> 是一个用于在编译时计算出特定类型(Type)的 size, alignment 以及 stride 的泛型类型。返回的数值以字节为单位。例如 Int16 类型的大小为 2 个字节,内存对齐为 2 个字节以及当我们需要连续排列多个 Int16 类型时,每一个 Int16 所需要占用的大小(stride)为 2 个字节。所有基本类型的 stride 都与 size 是一致的。

接下来,看看结构体类型的 MemoryLayout:

struct EmptyStruct {}
MemoryLayout<EmptyStruct>.size // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride // returns 1
struct SampleStruct {
 let number: UInt32
 let flag: Bool
}
MemoryLayout<SampleStruct>.size // returns 5
MemoryLayout<SampleStruct>.alignment // returns 4
MemoryLayout<SampleStruct>.stride // returns 8

空结构体的大小为 0,内存对齐为 1, 表明它可以存在于任何一个内存地址上。有趣的是 stride 为 1,这是因为尽管结构为空,但是当我们使用它创建一个实例的时候,它也必须要有一个唯一的地址。

对于 SampleStruct,它所占的大小为 5,但是 stride 为 8。这是因为编译需要为其填充空白的边界,使其符合它的 4 字节内存边界对齐。

再来看看类:

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 = 0
 let 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 中添加如下代码:

// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count

// 2
do {
 print("Raw pointers")

 // 3
 let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
 // 4
 defer {
 pointer.deallocate(bytes: byteCount, alignedTo: alignment)
 }

 // 5
 pointer.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)

 // 6
 let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
 for (index, byte) in bufferPointer.enumerated() {
 print("byte \(index): \(byte)")
 }
}

在这个代码段中,我们使用了 Unsafe Swift 指针去存储和读取两个整型数值。

接下来是对这段代码的解释:

1、声明了接下来都会用到的几个常量:

  • count 表示了我们要存储的整数的个数
  • stride 表示了 Int 类型的 stride
  • alignment 表示了 Int 类型的内存对齐
  • byteCount 表示占用的全部字节数

2、使用 do 来增加一个作用域,让我们可以在接下的示例中复用作用域中的变量名

3、使用 UnsafeMutableRawPointer.allocate 方法来分配所需的字节数。我们使用了 UnsafeMutableRawPointer,它的名字表明这个指针可以用来读取和存储(改变)原生的字节。

4、使用 defer 来保证内存得到正确地释放。操作指针的时候,所有内存都需要我们手动进行管理。

5、storeBytes load 方法用于存储和读取字节。第二个整型数值的地址通过对 pointer 的地址前进 stride 来得到。因为指针类型是 Strideable 的,我们也可以直接使用指针算术运算 (pointer+stride).storeBytes(of: 6, as: Int.self)。

6、UnsafeRawBufferPointer 类型以一系列字节的形式来读取内存。这意味着我们可以这些字节进行迭代,对其使用下标,或者使用 filtermap 以及 reduce 这些很酷的方法。缓冲类型指针使用了原生指针进行初始化。

使用类型指针

我们可以使用类型指针实现跟上面代码一样的功能,并且更简单:

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 = 42
 pointer.advanced(by: 1).pointee = 6
 pointer.pointee
 pointer.advanced(by: 1).pointee

 let 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 中添加如下代码:

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 = 42
 typedPointer.advanced(by: 1).pointee = 6
 typedPointer.pointee
 typedPointer.advanced(by: 1).pointee

 let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
 for (index, value) in bufferPointer.enumerated() {
 print("value \(index): \(value)")
 }
}

这段代码与上一段类似,除了它先创建了原生指针。我们通过将内存绑定(binding)到指定的类型上来创建类型指针。通过对内存的绑定,我们可以通过类型安全的方法来访问它。将我们手动创建类型指针的时候,系统其实自动帮我们进行了内存绑定。

获取一个实例的字节

很多时候我们需要从一个现存的实例里获取它的字节。这时可以使用 withUnsafeBytes(of:) 方法。

在 Playground 中添加如下代码:

do {
 print("Getting the bytes of an instance")

 var sampleStruct = SampleStruct(number: 25, flag: true)

 withUnsafeBytes(of: &sampleStruct) { bytes in
 for byte in bytes {
 print(byte)
 }
 }
}

这段代码会打印出 SampleStruct 实例的原生字节。withUnsafeBytes(of:) 方法可以获取到 UnsafeRawBufferPointer并传入闭包中供我们使用。

withUnsafeBytes 同样适合用 Array Data 的实例。

使用 Swift 操作指针的三大原则

当我们使用 Swift 操作指针的时候必须加倍小心,防止写出未定义行为的代码。下面是几个坏代码的示例。

不要从 withUnsafeBytes 中返回指针

 // Rule #1
do {
 print("1. Don't return the pointer from withUnsafeBytes!")

 var sampleStruct = SampleStruct(number: 25, flag: true)

 let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
 return bytes // strange bugs here we come ☠️☠️☠️
 }

 print("Horse is out of the barn!", bytes) /// undefined !!!
}

绝对不要让指针逃出 withUnsafeBytes(of:) 的作用域范围。这样的代码会成为定时炸弹,你永远不知道它什么时候可以用,而什么时候会崩溃。

一次只绑定一种类型

// Rule #2
do {
 print("2. Only bind to one type at a time!")

 let count = 3
 let stride = MemoryLayout<Int16>.stride
 let alignment = MemoryLayout<Int16>.alignment
 let byteCount = count * stride

 let 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>) in
 print(boolPointer.pointee) // See Rule #1, don't return the pointer
 }
}
**绝对不要**让一个内存同时绑定两个不同的类型。如果你需要临时这么做,可以使用 `withMemoryRebound(to:capacity:)` 来对内存进行重新绑定。并且,这条规则也表明了不要将一个基本类型(如 Int)重新绑定到一个自定义类型(如 class)上。不要做这种傻事。
### 不要操作超出范围的内存
```swift
// Rule #3... wait
do {
 print("3. Don't walk off the end... whoops!")

 let count = 3
 let stride = MemoryLayout<Int16>.stride
 let alignment = MemoryLayout<Int16>.alignment
 let byteCount = count * stride

 let 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 平台。

创建完成后,添加如下代码:

import Foundation

enum 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 termination
 var 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 的最后添加如下代码:

extension Integer {

 static var randomized: Self {
 let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
 return numbers.withUnsafeBufferPointer { bufferPointer in
 return bufferPointer.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
 return $0.pointee
 }
 }
 }

}

Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized

这里我们为 Integer 协议添加了一个静态属性,并为其提供了默认实现。我们首先获取了随机数,随后我们将获得字节数组重新绑定为所需要的类型,然后返回它的值。简单!

就这样,我们使用 unsafe Swift 实现了一个类型安全的随机器生成方法。

在日常开发中,我们并不会接触到很多直接操作内存的情境。但是掌握它的操作,能让我们在碰到类似代码里更加从容。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。

(0)

相关推荐

  • Swift与C语言指针结合使用实例

    Objective-C和C的API常常会需要用到指针.Swift中的数据类型都原生支持基于指针的Cocoa API,不仅如此,Swift会自动处理部分最常用的将指针作为参数传递的情况.这篇文章中,我们将着眼于在Swift中让C语言指针与变量.数组和字符串共同工作. ####用以输入/输出的参数指针 C和Objective-C并不支持多返回值,所以Cocoa API中常常将指针作为一种在方法间传递额外数据的方式.Swift允许指针被当作inout参数使用,所以你可以用符号&将对一个变量的引用作为指

  • Swift的74个常用内置函数介绍

    Swift包含了74个内置函数,但在 The Swift Programming Langage 一书中只介绍了其中的7个,其它的都没有在文档中体现. 这篇文章列举出了所有的Swift库函数.文中所谓的 内置函数 是指无需引入任何模块(比如说Fundation等)即可以直接使用的函数. 下面先来看看7个在文档中提到的库函数: 下面列出一些很实用,但未在文档中体现的库函数: 复制代码 代码如下: //断言,参数如果为`true`则继续,否则抛出异常 //assert mentioned on pa

  • Swift流程控制之循环语句和判断语句详解

    Swift提供了所有c类语言的控制流结构.包括for和while循环来执行一个任务多次:if和switch语句来执行确定的条件下不同的分支的代码:break和continue关键字能将运行流程转到你代码的另一个点上. 除了C语言传统的for-condition-increment循环,Swift加入了for-in循环,能更加容易的遍历arrays, dictionaries, ranges, strings等其他序列类型. Swift的switch语句也比C语言的要强大很多. Swift中swi

  • 浅谈在Swift中关于函数指针的实现

    Swift没有什么? 苹果工程师给我建的唯一一堵墙是:在Swift中没有任何办法获得一个函数的指针: 注意,C函数指针不会导入到Swift中(来自"Using Swift with Cocoa and Objective-C") 但是我们怎么知道这种情况下钩子的地址和跳到哪呢?让我们深入了解一下,并且看看Swift的func在字节码层面上的是什么. 当你给一个函数传递一个泛型参数时,Swift并没有直接传递它的地址,而是一个指向trampoline函数(见下文)并带有一些函数元数据信息

  • Swift中的指针操作和使用详细介绍

    Apple期望在Swift中指针能够尽量减少登场几率,因此在Swift中指针被映射为了一个泛型类型,并且还比较抽象.这在一定程度上造成了在Swift中指针使用的困难,特别是对那些并不熟悉指针,也没有多少指针操作经验的开发者(包括我自己也是)来说,在Swift中使用指针确实是一个挑战.在这篇文章里,我希望能从最基本的使用开始,总结一下在Swift中使用指针的一些常见方式和场景.这篇文章假定你至少知道指针是什么,如果对指针本身的概念不太清楚的话,可以先看看这篇五分钟C指针教程(或者它的中文版本),应

  • Swift中动态调用实例方法介绍

    在 Swift 中有一类很有意思的写法,可以让我们不直接使用实例来调用这个实例上的方法,而是通过类型取出这个类型的某个实例方法的签名,然后再通过传递实例来拿到实际需要调用的方法.比如我们有这样的定义: 复制代码 代码如下: class MyClass {     func method(number: Int) -> Int {         return number + 1     } } 想要调用 method 方法的话,最普通的使用方式是生成MyClass的实例,然后用.method来

  • Swift教程之枚举类型详解

    枚举定义了一个常用的具有相关性的一组数据,并在你的代码中以一个安全的方式使用它们. 如果你熟悉C语言,你就会知道,C语言中的枚举指定相关名称为一组整数值.在Swift中枚举更为灵活,不必为枚举的每个成员提供一个值.如果一个值(被称为"原始"的值)被提供给每个枚举成员,则该值可以是一个字符串,一个字符,或者任何整数或浮点类型的值. 另外,枚举成员可以指定任何类型,每个成员都可以存储的不同的相关值,就像其他语言中使用集合或变体.你还可以定义一组通用的相关成员为一个枚举,每一种都有不同的一组

  • Swift教程之字符串和字符详解

    一个字符串String就是一个字符序列,像"hello,world","albatross"这样的.Swift中的字符串是用String关键词来定义的,同时它也是一些字符的集合,用Character定义. Swift的String和Character类型为代码提供了一个快速的,兼容Unicode的字符解决方案.String类型的初始化和使用都是可读的,并且和C中的strings类似.同时String也可以通过使用+运算符来组合,使用字符串就像使用Swift中的其他基

  • swift中的正则表达式小结

    作为一门先进的编程语言,Swift 可以说吸收了众多其他先进语言的优点,但是有一点却是让人略微失望的,就是 Swift 至今为止并没有在语言层面上支持正则表达式. 正则表达式的用处: 判断给定的字符串是否符合某一种规则(专门用于操作字符串) - 电话号码,电子邮箱,URL... - 可以直接百度别人写好的正则 - 别人真的写好了,而且测试过了,我们可以直接用 - 要写出没有漏洞正则判断,需要大量的测试,通常最终结果非常负责 过滤筛选字符串,网络爬虫 替换文字,QQ聊天,图文混排 语法规则 使用过

  • Swift中的可变参数函数介绍

    可变参数函数指的是可以接受任意多个参数的函数,我们最熟悉的可能就是 NSString 的 -stringWithFormat:方法了.在 Objective-C 中,我们使用这个方法生成字符串的写法是这样的: 复制代码 代码如下: NSString *name = @"Tom"; NSDate *date = [NSDate date]; NSString *string = [NSString stringWithFormat:                 @"Hell

随机推荐