在 Swift 里使用指针

Jun 28, 2018 at 13:38:05

最近做的事情需要在 Swift 里调用 C 函数,要学的东西很多,经历颇为曲折,遂作此笔记以备忘。

1. 什么是指针?

参考这里: The 5-Minute Guide to C Pointers

或者中文版: C 语言指针 5 分钟教程 - 文章 - 伯乐在线

2. Swift 里的不安全操作

Swift 大部分操作都是安全的,Optional 的引入也是为了安全。大部分情况下编译器会帮我们做静态校验,一个 Int 类型不能被当做一个 String 传参,一个 Optional 类型必须 Unwrapped 之后才能安全使用。

但是 Swift 也支持对内存的直接操作,这些操作都是不安全的,所以涉及的 API 都带有 Unsafe 前缀,比如指针 UnsafePointer 类型。

通过下标访问数组内容也属于不安全操作,比如:

let arr = []
let test = arr[1]

编译时不会报错,运行时 crash。

3. MemoryLayout

C/C++ 里常见的 sizeof() 关键字在 Swift 里面原先也有,以及 sizeofValue, strideof, strideofValue, alignalignOf。在 Swift 3 中,这一堆“函数”被包进了 MemoryLayout 这个 Struct 里面,这里是当时的提案

于是和内存对齐内存占用等相关的信息就都可以通过 MemoryLayout 获取,比如:

MemoryLayout<UInt32>.size // 4
MemoryLayout<UInt32>.alignment // 4
MemoryLayout<UInt32>.stride // 4

UInt32 默认占 4 个字节,内存对齐为 4 字节。

连续内存里多个实例排列时(比如数组),上一个实例开始地址到下一个实例开始地址的距离为 4 个字节。

在我的 64-bit 机器上,Int 类型则和 UInt64 类型一样,占 8 个字节。

内存对齐

为什么需要做内存对齐可以参考这里:Purpose of memory alignment - Stack Overflow

理论上物理内存的单位是一个字节(byte),我们最小能访问的内存就应该是一个字节。但是实际上为了效率考虑,或者硬件限制,我们访问内存总是字长的倍数,字长由设备来决定,比如某机器 64 位机器以 8 字节为字长,这样设备每次访问内存都会以 8 字节为单位。如果内存里的某个值不在 8 字节的边界上,那么处理器想要获取这个值就得访问两次内存,为了性能考虑通常编译器会对这些值做内存对齐,用空间换时间。

另外 App 能访问的内存地址都不是物理地址,而是通过操作系统访问到的虚拟地址。

比如下面这个 Struct

struct SampleStruct {
  let number: UInt32
  let flag: Bool
}

MemoryLayout<SampleStruct>.size // returns 5
MemoryLayout<SampleStruct>.alignment // returns 4
MemoryLayout<SampleStruct>.stride // returns 8

UInt32 长度 4,Bool 长度 1,总长其实是 5。但是因为内存对齐的存在,所以整个 Struct 塞进内存里会以 4 字节对齐,结果占用 8 个字节(64位机器)。

StructInt 一样是值类型 (Value Type),但如果是引用类型(Reference Type) 比如类 class,就不太一样。

Swift class 在底层实现事实上是一个 Objectivce C Class(根据 mikeash 的这个视频 Xode 8, Swift 3),我们创建一个新的 Swift class 实例的时候,会在堆上 (heap) 分配一块比较大的内存,用来保存诸如 type, reference count之类的信息,在栈上 (stack) 只分配一个指针,指向堆上的这块内存。所以对一个 class 执行 MemoryLayout 效果如下:

class SampleClass {
  let number: UInt32
  let flag: Bool
}

MemoryLayout<SampleClass>.size // returns 8
MemoryLayout<SampleClass>.alignment // returns 8
MemoryLayout<SampleClass>.stride // returns 8

4. Swift Pointer

Swift 一共有 8 种指针类型。基础是不可变指针 UnsafePointer,对应的会有可变指针 UnsafeMutablePointer。另外还有可以塞数组的 Buffer 指针和完全不知道是什么内容的 Raw 指针,也即没有指定泛型。

四种带类型指针:

  • UnsafeMutablePointer
  • UnsafePointer
  • UnsafeMutableBufferPointer
  • UnsafeBufferPointer

四种不带类型的 RawPointer:

  • UnsafeMutableRawPointer
  • UnsafeRawPointer
  • UnsafeMutableRawBufferPointer
  • UnsafeRawBufferPointer

Apple Developer 文档里有 C 指针和 Swift 指针的对应表:

C Syntax Swift Syntax
const Type * UnsafePointer
Type * UnsafeMutablePointer
Type * const * UnsafePointer
Type * __strong * UnsafeMutablePointer
Type ** AutoreleasingUnsafeMutablePointer
const void * UnsafeRawPointer
void * UnsafeMutableRawPointer

5. 特殊指针

上面提到的八种指针都比较常见,比如 const int * 对应 Swift UnsafePointer<Int32>。那如果有些比较复杂的需求上述指针无法满足的怎么办呢?

OpaquePointer

如果一个 C 指针类型无法在 Swift 中找到对应的类型,则可以用这个指针来表达,比如一个类型为 C Struct 的指针。

该指针的初始化方法里有一个比较特别的方法:

init?(bitPattern: Int)
    // Creates an OpaquePointer from a given address in memory.

可以用一个内存地址来初始化这个指针。举个例子,现在 Swift 要调用 C 函数,传入一个 context 指针,回调的时候 C 函数会把这个指针通过参数带回给 Swift,相当于 ObjC 常见的 userinfo。我们可以这样做:

// 把 self 用 OpaquePointer 指针表达
let pointer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())

// 传给 C 函数...
// 在 C 函数的回调里面取会这个指针
let someObj = Unmanaged<ObjectClass>.fromOpaque(context!).takeUnretainedValue()

6. 小结

指针非常强大,在 C/C++/ObjC 语言中也是使用非常广泛的工具。但是同时直接操作内存也给程序带来非常高的风险。项目简单的时候问题不大,一旦复杂起来问题就很容易被淹没在茫茫代码里。

Swift 的设计是倾向安全的,我们平时会用到的大部分特性都不需要和裸的内存数据打交道,所有的指针,内存绑定等事情都由已经封装好的高级类型帮我们搞定了。

但是 Swift 也提供了不安全的内存操作 API,尽管相比起 C 接口,Swift 已经尽量做了相对安全的封装,但我们在使用这些 API 的过程中仍然需要小心谨慎。

想要在使用过程中不犯错就必须先理解这些不安全的操作都做了什么,必须理解指针,理解内存,理解这些操作在 C/C++/Swift 里的差别。@mikeash 的演讲 Exploring Swift Memory Layout 讲得非常好,值得一看。

7. 参考资料

Tags: