UnsafePointer
现阶段想要完全抛弃 C 的一套东西还是相当困难的,特别是在很多上古级别的 C API 框架还在使用(或者被间接使用)。开发者,尤其是偏向较底层的框架的开发者不得不面临着与 C API 打交道的时候,还是无法绕开指针的概念,而指针在 Swift 中其实并不被鼓励,语言标准中也是完全没有与指针完全等同的概念的。
为了与庞大的 C 系帝国进行合作,Swift 定义了一套对C语言指针的访问和转换方法,那就是 UnsafePointer
和它的一系列变体。对于使用 C API 时如果遇到接受内存地址作为参数,或者返回是内存地址的情况,在 Swift 里会将它们转为 UnsafePointer
的类型,比如说如果某个 API 在 C 中是这样的话:
void method(const int *num) {
printf("%d", *num)
}
其对应的 Swift 方法应该是:
func method(_ num: UnsafePointer<CInt>) {
print(num.pointee)
}
这里的 UnsafePointer
, 就是 Swift 中专门针对指针的转换。对于其他的 C 中基础类型,在 Swift 中对应的类型都遵循统一的命名规则:在前面加上一个字母 C
并将原来的第一个字母大写:比如 int
,bool
和 char
的对应类型分别是 CInt
,CBool
和 CChar
。在上面的 C 方法中,我们接受一个 int
的指针,转换到 Swift 里所对应的就是一个 CInt
的 UnsafePointer
类型。这里原来的 C API 中已经指明了输入的 num
指针的不可变的(const),因此在 Swift 中我们与之对应的是 UnsafePointer
这个不可变版本。如果只是一个普通的可变指针的话,我们可以使用 UnsafeMutablePointer
来对应:
C API | Swift API |
---|---|
const Type * | UnsafePointer |
Type * | UnsafeMutablePointer |
在 C 中,对某个指针进行取值使用的是 *
,而在 Swift 中我们可以使用 pointee
属性来读取相应内存中存储的内容。通过传入指针地址进行方法调用的时候就都比较相似了,都是在前面加上 &
符号,C 的版本和 Swift 的版本只在声明变量的时候有所区别:
// C
int a = 123;
method(&a); //123
//Swift
var a: CInt = 123
method(&a) //123
遵守这些原则,使用在Swift中进行C API的调用应该就不会有很大问题了。
另外一个重要的课题是如何在指针的内容和实际的值之间进行转换。比如我们如果由于某种原因需要涉及到直接使用 CFArray
的方法来获取数组中元素的时候,我们会用到这个方法:
func CFArrayGetValueAtIndex(_ theArray: CFArray!, _ idx: CFIndex) -> UnsafeRawPointer!
// UnsafeRawPointer 在 Swift 3.0 以前是 UnsafePointer<Void> 也就是 void *
因为 CFArray
中是可以存放任意对象的,所以这里的返回是一个任意对象的指针,相当于 C 中的 void *
。这显然不是我们想要的东西。Swift 中为我们提供了一个强制转换的方法 unsafeBitCast
,通过下面的代码,我们可以看到应当如何使用类似这样的 API,将一个指针强制按位转成所需类型的对象:
let arr = NSArray(object: "meow")
// unsafeBitCast 将第一个参数的内容按照第二个参数的类型进行转换
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), to: CFString.self) //str: CFString
//str = meow
unsafeBitCast
会将第一个参数的内容按照第二个参数的类型进行转换,而不去关心实际是不是可行,这也正是 UnsafePointer
的不安全所在,因为我们不必遵守类型转换的检查,而拥有了在指针层面直接操作内存的机会。
其实说了这么多,Apple 将直接的指针访问冠以 Unsafe
的前缀,就是提醒我们: 这些东西不安全,亲们能不用就别用了吧(作为Apple,另一个重要的考虑是如果避免指针的话可以减少很多系统漏洞)!在日常开发中,我们确实不太需要经常和这些东西打交道(除了传入指针这个历史遗留问题以外,而且在Swift 2.0中也已经使用异常机制替代了)。总之,尽可能地在 高抽象层级编写代码,会是高效和正确的有力保证。无数先辈已经用血淋淋的教训告诉我们,要避免去做这样的不安全的操作,除非你确实知道你在做的是什么。
UnsafeMutablePointer
是不可变指针, UnsafePointer
对应的是可变指针。
Unsafe[Mutable]RawPointer
在 Swift 3.0 以前是 Unsafe[Mutable]Pointer<Void>
也就是C 中的 void *
。
Unsafe[Mutable]BufferPointer
表示一连串的数组指针
C 指针内存管理
C指针在Swift中被冠名以 Unsafe
的另一个原因是无法对其进行自动的内存管理。和 Unsafe
类的指针工作的时候,我们需要像ARC时代之前那样手动地来申请和释放内存,以保证程序不会出现泄露或是因为访问已释放内存而造成崩溃。
我们如果想要声明,初始化,然后使用一个指针的话,完整的做法是使用 allocate
和 initialize
来创建。如果一不小心,就很容易写成下面这样:
class MyClass {
var a = 1
deinit {
print("deinit")
}
}
var pointer: UnsafeMutablePointer<MyClass>!
pointer = UnsafeMutablePointer<MyClass>.allocate(capacity: 1)
pointer.initialize(to: MyClass())
print(pointer.pointee.a) //1
pointer = nil
虽然我们最后将 pointer
设为 nil
,但是由于 UnsafeMutablePointer
并不会自动进行内存管理,因此其实 pointer
所指向的内存是没有被释放和回收的(这可以从 MyClass
的 deinit
没有被调用来加以证实; 这造成了内存泄露。正确的做法是为 pointer
加入 deinitialize
和 deallocate
,它们分别会释放指针指向的内存的对象以及指针自己本身:
class MyClass {
var a = 1
deinit {
print("deinit")
}
}
var pointer: UnsafeMutablePointer<MyClass>!
pointer = UnsafeMutablePointer<MyClass>.allocate(capacity: 1)
pointer.initialize(to: MyClass())
print(pointer.pointee.a) //1
pointer.deinitialize()
pointer.deallocate(capacity: 1)
pointer = nil
//输出
//1
//deinit
如果我们在 deallocate
之后再去访问 pointer
或者再次调用 deallocate
的话,迎接我们的自然是崩溃。在手动处理这类指针的内存管理时,我们需要遵循的一个基本原则就是谁创建谁释放。deallocate
与 deinitialize
应该要和 allocate
与 initialize
成对出现,如果不是你创建的指针,那么一般来说你就不需要去释放它。一种最常见的例子就是如果我们是通过调用了某个方法得到的指针, 那么除非文档或者负责这个方法的开发者明确告诉你应该由使用者进行释放,否则都不应该去试图管理它的内存状态:
var x: UnsafeMutablePointer<tm>!
var t = time_t()
time(&t)
x = localtime(&t)
x = nil
最后,虽然在本节的例子中使用的都是 allocate
和 deallocate
的情况,但是指针的内存申请也可以使用 malloc
或者 calloc
来完成,这种情况下在释放时我们需要对应使用 free
而不是 deallocate
。
COpaquePointer 和 C convention
在 C 中有一类指针,你在头文件中无法找到具体的定义,只能拿到类型的名字,而所有的实现细节都是隐藏的。这类指针在 C 或C++ 中被叫做不透明指针(Opaque [əʊˈpeɪk] Pointer),顾名思义,它的实现和表意对使用者来说是不透明的。
在Swift中对应这类不透明指针的类型是 COpaquePointer
,它用来表示那些在 Swift 中无法进行类型描述的C指针。那些能够确定类型的指针所表示的是其指向的内存是可以用某个Swift中的类型来描述的,因此都使用更准确的 UnsafePointer<T>
来存储。而对于另外那些 Swift 无法表述的指针,就统一写为COpaquePointer
,以作补充。
在Swift早期beta的时候,曾经有不少API返回或者接受的是 COpaquePointer
类型。但是随着 Swift 的逐渐完善,大部分涉及到指针的API里的都被正确地归类到了合适的 Unsafe
指针中,因此现在在开发中可能很少能再看到 COpaquePointer
了。最多的使用场景可能就是 COpaquePointer
和某个特定的 Unsafe
之间的转换了,我们可以分别使用这两个类型的初始化方法将一个指针转换从某个类型强制地转为另一个类型:
public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
//...
public init(_ from: OpaquePointer)
//...
}
public struct OpaquePointer : Hashable {
//...
public init<T>(_ from: UnsafePointer<T>)
//...
}
COpaquePointer
在 Swift 中扮演的是指针转换的中间人的角色,我们可以通过这个类型在不同指针类型间进行转换。当然了,这些转换都是不安全的,除非你知道自己在干什么,以及有十足的把握,否则不要这么做!
另外一种重要的指针形式是指向函数的指针,在 C 中这种情况也并不少见,即一块存储了某个函数实际所在的位置的内存空间。从Swift 2.0 开始,与这类指针可以被转化为闭包,不过和其他普通闭包不同,我们需要为它添加上 @convention
标注。
举个例子,如果我们在 C 中有这样一个函数:
int cFunction(int (callback)(int x, int y)) {
return callback(1, 2);
} //在.c 文件
这个函数接受一个 callback,这个 callback 有两个 int
类型的参数,cFunction
本身返回这个 callback
的结果。如果我们想在 Swift 中使用这个 C 函数的话,应该这样写:
let callback:@convention(c) (Int32, Int32) -> Int32 = {
(x, y) -> Int32 in
return x + y
}
let result = cFunction(callback)
print(result)// 3
如果不添加 @convention
,会报错:
A C function pointer can only be formed from a reference to a 'func' or a literal closure
在没有歧义的情况下,我们甚至可以省掉这个标注, 而直接将它以一个 Swift 闭包的形式传递给 C :
let result1 = cFunction { (x, y) -> Int32 in
return x * y
}
print(result1)//2
完美,你甚至感觉不到自己在和 C 打交道 !