不安全的Swift

421 阅读8分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

安全与不安全

在文章的开始,我们先来明确下什么是安全不安全,安全和不安全在接口设计上的区别并不明显,最大的区别在于实现处理无效输入的方式。在Swift标准库中,只有一小部分被明确标记为“不安全”(下图高亮部分为不安全)

截屏2022-02-03 下午7.50.54.png

安全性操作:大多数操作在执行之前都会完全验证其输入,因此我们可以放心地假设我们可能犯的任何严重的编码错误都会被可靠地捕获和报告。

截屏2022-02-03 下午5.23.12.png

安全的强制解包

举个🌰:

let value: Int? = nil
print(value!)

运行之后,会crash并抛出Fatal error: Unexpectedly found nil while unwrapping an Optional value错误。 在这里,尝试强制解包nil值,是一个严重的编程错误,但其后果是明确定义的,我们可以完全描述它对所有可能输入的行为,包括不满足其要求的输入。所以我们说force unwrap 操作符是安全的。

不安全的强制解包

let value: Int? = nil
print(value.unsafelyUnwrapped)

Optional类型的数据,提供了unsafelyUnwrapped属性来强制展开操作,就像常规的force-unwrap运算符一样,这也要求基础值非零。但当值为nil时,此时,它读取的值不存在,很难说这到底意味着什么,根据任意情况,它可能会立即触发崩溃,或者可能会返回一些垃圾值,也可能会做其他事情。如果使用这个属性,你就需要对这个事情的结果负责,它可能会带来一些不可预测的影响,也会使调试问题变得复杂。

截屏2022-02-03 下午10.32.27.png

安全API的目标不是防止崩溃,正相反:它们在已知的输入之外,安全API会通过引起致命的崩溃报告让我们知道问题是如何发生的,以便我们可以调试和纠正问题。

我们说 Swift是一种安全的编程语言时,意思是:在默认情况下,我们使用该语言和系统库功能时,会完全验证它们所有的输入,所有不被允许的操作都会被明确的标记为不安全

不安全指针

Swift标准库里面提供了强大的不安全指针类型,它们与C语言的指针大致处以同一抽象级别

内存模型

Swift中有一个flat memory model,它将内存视为可单独寻址的8位字节的线性地址空间,这些字接种的每一个都有自己唯一的地址,通常打印为十六进制整数值。 在运行时,内存地址空间很少的一部分会被填充,来反映我们应用程序在任意时刻的执行状态,包括:App的可执行二进制文件、我们引用的系统动态库、堆栈、本地和临时变量的存储空间、动态内存区域(类实例存储和我们手动分配的内存)及一些资源文件(图片等)

截屏2022-02-03 下午5.37.08.png

每个单独的项目都分配有一段连续的内存区域,随着App运行,内存状态会不断发展,新对象被分配,堆栈不断变化,Swift语言和运行时负责跟踪内存变化,一般不需要我们手动管理内存

同样的,Swift中也提供了一些管理内存的的低级API,于此同时,需要你自己来管理该处内存,如果访问了一个已经被释放的指针,就会产生一些严重的编程错误,我们看下如下示例:

let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1) //申请 一个 Int类型大小的内存。
ptr.initialize(to: 42) //内存中初始值为 42
ptr.deallocate() // 释放内存
ptr.pointee = 23 // UNDEFINED BEHAVIOR
print(ptr.pointee)

输出结果为

5764607523303012114
Program ended with exit code: 0

我们看到,此时的指针值已经发生了一些我们无法预知的结果了。

既然指针是如此危险,为什么还要使用它们呢?一个很重要的原因就是:与C或Objective-C等不安全语言的互操作性。

与C或OC的互操作性

互操作性

C或者OC中,经常把指针作为参数,为了能在swift中调用,我们需要知道如何生成Swift指针,下图是C语言指针和Swift不安全指针的关系对应表。

截屏2022-02-03 下午5.44.06.png

举个🌰:

已知如下C函数,在导入到Swift中之后,函数声明的格式就变成了如下形式。

void process_integers(const int *start, size_t count)
import Darwin.C.stdio
public func process_integers(_ start: UnsafePointer<Int32>!, _ count: Int)

另外,我们可以使用allocate方法,动态的开辟内存,去存储整型数据。我们可以使用指针一步一步的往内存缓冲区中添加数据。

let start = UnsafeMutablePointer<CInt>.allocate(capacity: 4)// 1

start.initialize(to: 0) //2
(start + 1).initialize(to: 2)
(start + 2).initialize(to: 4)
(start + 3).initialize(to: 6)

process_integers(start, 4) //3
start.deinitialize(count: 4) // 4
start.deallocate() //5
  • 1,申请生成4个连续CInt类型大小的内存空间,并得到初始指针。
  • 2,依次向内存中存入0 2 4 6
  • 3,调用C语言方法。
  • 4,销毁内存中的值。
  • 5,释放内存,以便于可以重复该处内存。

我们可以完全自己控制每一个操作,但从根本上说这是不安全的:分配的缓冲区的生命周期不是由返回指针管理,我们必须在适当的时候手动释放它,否则,它将永远存在,导致内存泄漏

初始化不能自动校验是否在已初始化空间的指针,如果我们搞错了,我们就会得到未定义的行为。这段代码有一个问题就是,只有缓冲区的起始地址,它的长度是一个完全独立的值

缓冲区边界

可以通过将缓冲区建模(起始地址,长度)来提高代码的清晰度,可以很容易获得缓冲区的边界,在任何时候可以轻松检查越界访问。 在Swift标准库中,有四种不安全缓冲区指针类型:UnsafeBufferPointer<Element>、UnsafeMutableBufferPointer<Element>、UnsafeRawBufferPointer、UnsafeMutableRawBufferPointer,当我们需要使用内存区域而不是指向单个值的指针时,这4种类型都是不错的选择

截屏2022-02-04 上午10.38.59.png 可以通过上面4个方法,获取一个指向单个 Swift 值的临时指针,然后我们可以将其传递给C函数。我们可以使用这些方法来简化我们的代码,将不安全的操作隔离到尽可能小的代码部分。

let values: [CInt] = [0, 2, 4, 6]

values.withUnsafeBufferPointer { buffer in
  print_integers(buffer.baseAddress!, buffer.count)
}

为了摆脱手动内存管理的需要,我们可以将输入数据存储在一个数组值中。然后可以使用 withUnsafeBufferPointer 方法临时获得对数组底层存储的直接访问。在我们传递给这个函数的闭包中,我们可以得到起始地址计数值,并将它们直接传递给我们想要调用的 C 函数。

let values:[CInt] = [0,2,4, 6]
process_integers(values, values.count)

事实上,向C语言传递指针是非常频繁的,在Swift中,为此提供了特殊的语法,可以简单地将数组值传递给不安全的指针函数,编译器会自动为我们生成等价的withUnsafeBufferPointer。下图是 Swift支持的此类隐式值到指针转换的列表

截屏2022-02-03 下午5.57.36.png

虽然使用inout到指针很方便,但它们实际上只是为了帮助调用C函数,在Swift代码中,我们需要更少的传递指针,因此,我们更喜欢使用基于闭包的API,基于闭包的设计使结果指针的实际生命周期更加明确,帮助您避免生命周期问题。

截屏2022-02-04 上午11.32.33.png

此外,在Swift标准库中,允许我们通过直接将数据复制到其底层未初始化存储中来创建 ArrayString 值。这消除了仅为准备此类数据而分配临时缓冲区的需要

截屏2022-02-04 上午11.35.00.png

总结

要有效的使用不安全的API,我们需要明确的知道期望结果是什么,并了解到所有可能发生的情况。否则代码将会出现一些未定义的行为当使用包含多个元素的内存区域时,最好使用不安全的缓冲区指针而不是指针值来跟踪其边界

本文介绍了,什么是安全和不安全,SwiftC语言指针的互操作性以及不安全缓冲区的使用。

如果觉得有收获请按如下方式给个 爱心三连:👍:点个赞鼓励一下。🌟:收藏文章,方便回看哦!。💬:评论交流,互相进步!