Unsafe Swift
Swift & UI 高级 18m

深入理解 Unsafe Swift

Unsafe Swift

2020年6月25日

在 Apple 官方观看视频

一句话判断

Swift 的 Unsafe 系列指针不是「危险请勿触碰」——它们是性能敏感场景下的必要工具,只要你搞懂内存布局规则和所有权语义,用起来其实没那么可怕。

这场 Session 讲了什么

这场 Session 深入讲解了 Swift 中所有以 Unsafe 开头的类型:UnsafePointerUnsafeMutablePointerUnsafeBufferPointerUnsafeRawPointerUnsafeMutableRawPointer 以及 UnsafeFunctionPointer 等。

核心议题包括:Swift 的内存布局(MemoryLayout)、指针与 Swift 数组/字符串之间的转换、如何安全地使用 withUnsafeByteswithUnsafeMutableBytes、raw pointer 的字节操作、以及指针算术。Session 花了大量篇幅解释「为什么这些类型叫 Unsafe」——不是因为它们会随机崩溃,而是因为编译器不再为你做边界检查和生命周期管理。

对于需要与 C API 交互、处理二进制数据协议、或者做高性能计算的开发者,这是必修内容。

值得深挖的点

Typed Pointer vs Raw Pointer 的选择

UnsafePointer<T> 是类型化的,你知道每个元素是 T 类型的,字节对齐和步长由编译器自动计算。UnsafeRawPointer 是无类型的字节流,你需要自己管理对齐和偏移量。Session 的建议是:优先使用 Typed Pointer,只在处理二进制协议或需要 reinterpret cast 时才用 Raw Pointer。从 Raw Pointer 转换到 Typed Pointer 用 assumingMemoryBound(to:) 或者 bindMemory(to:capacity:)——前者是「我保证这块内存已经是 T 类型」,后者是「我要求编译器把这块内存重新绑定为 T 类型」。

指针的生命周期陷阱

withUnsafePointerwithUnsafeBufferPointer 的闭包参数返回后,指针就失效了。这个规则很多开发者会违反——把指针存到闭包外面、放到异步回调里用。Session 明确指出:逃逸指针是未定义行为,不是「可能崩溃」,是「可能看起来正常工作但在特定优化级别下爆炸」。

代码片段

// 正确使用 withUnsafeBytes 访问数据缓冲区
func processImageData(_ data: Data) {
    data.withUnsafeBytes { rawBuffer in
        // rawBuffer 是 UnsafeRawBufferPointer
        let rawPointer = rawBuffer.baseAddress!
        
        // 假设数据是 RGBA 像素数组,每个像素 4 字节
        let pixelCount = rawBuffer.count / 4
        let pixels = rawPointer.bindMemory(to: UInt32.self, capacity: pixelCount)
        
        // 现在可以像数组一样访问像素值
        for i in 0..<pixelCount {
            let rgba = pixels[i]
            let r = rgba & 0xFF
            let g = (rgba >> 8) & 0xFF
            // 处理像素...
        }
    }
    // 闭包结束后 rawBuffer 失效,不能逃逸使用
}
// UnsafeMutableRawPointer 的手动内存管理
func allocateCustomBuffer() {
    // 分配 1024 字节的原始内存
    let byteCount = 1024
    let alignment = MemoryLayout<Int>.alignment
    let rawPtr = UnsafeMutableRawPointer.allocate(
        byteCount: byteCount,
        alignment: alignment
    )
    
    // 方式一:存储 Int 值
    let intPtr = rawPtr.bindMemory(to: Int.self, capacity: byteCount / MemoryLayout<Int>.size)
    intPtr[0] = 42
    intPtr[1] = 100
    
    // 方式二:直接用 raw pointer 的 store/load
    rawPtr.storeBytes(of: 0xABCD, as: UInt16.self)
    let value = rawPtr.load(as: UInt16.self)
    
    // 用完必须手动释放!
    rawPtr.deallocate()
}
// 在 Swift 中处理 C 函数指针回调
// 假设 C 库有个函数: void process(void *ctx, void (*callback)(void *, int result))

typealias CallbackType = @convention(c) (UnsafeMutableRawPointer?, Int32) -> Void

func bridgeToC() {
    // Swift 闭包不能直接传给 C 函数指针
    // 需要把上下文对象装箱传过去
    let context = MyContext()
    
    // 把 Swift 对象引用封装为 UnsafeMutableRawPointer
    let contextPtr = Unmanaged.passUnretained(context).toOpaque()
    
    let callback: CallbackType = { ctx, result in
        // 在回调中恢复 Swift 对象
        guard let ctx = ctx else { return }
        let obj = Unmanaged<MyContext>.fromOpaque(ctx).takeUnretainedValue()
        obj.handleResult(Int(result))
    }
    
    // c_library_process(contextPtr, callback)
}

最佳实践

  1. 永远用 withUnsafe* 系列方法,而不是手动分配和释放指针。Swift 的 closure-scoped 指针有明确的生命周期,不容易出错。只有在你确实需要跨作用域持有指针时才手动 allocate。
  2. MemoryLayout<T>.size 而不是硬编码大小。不同平台上的类型大小可能不同(比如 Int 在 32 位和 64 位系统上不一样)。MemoryLayout 是编译期常量,不会影响性能。
  3. Debug 模式下开启 Address Sanitizer。如果你在用 Unsafe 类型,Product > Scheme > Edit Scheme > Diagnostics 里打开 Address Sanitizer,它能帮你捕获大部分的越界和 use-after-free 问题。

还有什么值得关注

  • Swift 5.3 改进了与 C 函数指针的互操作性,部分场景下可以直接把 Swift 闭包传给 C 回调。
  • UnsafeMutablePointerinitializedeinitialize 是必须配对调用的,否则不会触发 ARC 的引用计数管理——对于包含 class 实例的缓冲区来说,漏掉 deinitialize 就是内存泄漏。
  • Session 提到未来的 Swift 版本可能会引入有所有权的指针类型(Ownership Manifesto 的一部分),届时指针的生命周期管理会更安全。
WWDC 2020