任何语言的底层实现,其实都离不开指针,大部分高级语言都会将指针的操作隐匿起来,比如在Swift中我们很少会直接使用指针。但是这并不意味着我们在Swift中不能使用指针了,只是使用起来会更加麻烦 — 高层建筑总能能带来便利性,但是,这将牺牲它的灵活性 — 没有什么比指针更加灵活了。
由于Swift它是一门严格安全的语言,如果在Swift的框架之下,我们在编程中使用到的内存管理将由Swift自动处理,而指针则游离在此之外,也就是说,我们需要自己去管理指针占有内存的管理,这将回到更加古老的编程手段中,显然,在Swift中使用指针并不便捷,同时也不安全。
Swift当然考虑到了这一点,于是增添了一系列和指针有关的类和结构,目的是让我们更轻松的在Swift中使用指针。
Swift中有两大类的指针: typed pointer
指定类型指针,raw pointer
未指定类型的指针。不管是typed pointer
还是raw pointer
,他们的几个子类都是以unsafe
开头,这同时也在告知使用者,在Swift 中使用指针是不安全的。
一、typed point
typed point
是指定类型的指针,我们在使用typed point
的使用,会明确知道该指针指向的类型。它包含了一共四个子类型,他们分别是:
- UnsafePointer
- UnsafeMutablePointer
- UnsafeBufferPointer
- UnsafeMutableBufferPointer
typed point
指定了类型,也即相当于我们在C语言中类似的指明了类型指针。
1. UnsafePointer
UnsafePointer 的官方描述是 访问特定类型数据的指针。意即这个指针的类型是已知。
在上一篇文章中了解到,内存的三种状态:
- 未绑定类型同时未初始化值
- 绑定类型但是未初始化值
- 绑定类型同时初始化值
每一种状态都必须要有特定的指针区表示。这里的UnsafePointer就是指向绑定类型同时初始化值内存的。
另外,它相当于C系列语言中的const
对象,我们不能直接初始化UnsafePointer
对象(当然如果指定地址是可以的,但是我们在实际开发中,我们不太可能直接使用这个方法初始化一个指针。因为这样初始化的指针很不透明,你必须要确定对应的地址存放的数据类型是对应的,不然得到的将是一个位置的值)。同时我们也不能修改UnsafePointer
指向的值,因为这是一个常量指针!
// 通过地址初始化,也即是让这个指针指向某个地址,因为我们仅仅制定了指针的类型,并没有给指针初始化一块内存,所以它指向一个位置的东西。
let usp = UnsafePointer<Int>.init(bitPattern: 20)
print(usp.pointee) // 这里的打印是没有意义的
//通过另一个变量指针初始化一个`UnsafePointer`常量指针
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
usmp.pointee = 20
let usp2 = UnsafePointer<Int>.init(usmp)
print(usp2.pointee) // 20
我们可以通过 pointee
属性发访问指针的值。但是要注意,UnsafePointer
并不能对值进行设置,如果需要可以设置指向值的指针,应该使用UnsafeMutablePointer
。
当然我们还可以直接打印指针,得到这个指针指向的地址。
print(usp2) // 0x0000000133d08340
2. UnsafeMutablePointer
上面已经做了一些对UnsafeMutablePointer
的描述,它相当于C系列语言中的变量指针。 :
C语言中:
int *p = malloc(sizeof(int));
Swift中创建一个UnsafeMutablePointer:
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
两者的意义相同。
我们同样可以通过 pointee
属性发访问指针的值。同时,你还可以直接修改 pointee
的值,就像我们使用Swift的其他类一样。
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
usmp.pointee = 100
print(usmp.pointee) // 100
3. UnsafeBufferPointer
UnsafeBufferPointer
指代一系列连续的内存。我们可以使用这个指针作为一个序列的指针,并通过指针直接访问旗下元素。
let intArr = [10,20,30,40,50]
let usbp = UnsafeBufferPointer.init(start: intArr, count: 4) // 截取了四个元素
print(usbp[0],usbp[1],usbp.count,usbp.last ?? "0")
usbp.forEach { (a) in
print(a)
}
//
10 20 4 Optional(40)
10
20
30
40
尽管这是一个Swift 的指针,但是我们依然像使用对象一样操作了数组,因为UnsafeBufferPointer 遵循了Collection 协议,说明这是一个可变的集合,对于集合我们进行遍历则无可厚非了。
但是同样的,这个UnsafeBufferPointer
是常量,它只能获取到数据,但是通过这个指针去修改数据。与之对应的是UnsafeMutableBufferPointer
指针。
4. UnsafeMutableBufferPointer
可变的系列指针。UnsafeMutableBufferPointer
拥有对指向序列修改的能力:
let usmp = UnsafeMutablePointer<Int>.allocate(capacity: sizeof_sfntInstance)
let usmbp = UnsafeMutableBufferPointer<Int>.init(start: usmp, count: 5) // 拓展为5各元素的大小
usmbp[0] = 120
usmbp[1] = 130 //进行修改 其他未修改的内容将产生随机值
usmbp.forEach { (a) in
print(a)
}
print(usmbp.count)
状况跟UnsafeBufferPointer
有点类似,只是在初始化的时候,需要借助UnsafeMutablePointer
。 并不能直接使用已经存在序列进行初始化。
值的注意的是:如果一个序列被初始化之后,没有给每一个元素赋值的话,这些元素的值都将出现随机值。
二、raw pointer
raw pointer
指未知类型的指针,这个类型相当于C语言中的(void *
)类型,。
同样的,raw pointer
也包含了四个子类型:
- UnsafeRawPointer
- UnsafeMutableRawPointer
- UnsafeBufferRawPointer
- UnsafeMutableBufferRawPointer
1. UnsafeRawPointer和UnsafeMutableRawPointer
UnsafeRawPointer
类型不提供自动内存管理,也不保证其内存,同时没有做任何的内存对齐。 开发者在使用UnsafeRawPointer
的时候,应该手动对指针做内存管理,以避免泄漏或未定义的行为。这一点和UnsafePointer
类似,但是相对来说UnsafeRawPointer
更加灵活,我们可以单纯的创建一个指针,而不管它的类型和值。当需要使用来操作某个内存的时候,再来绑定类型和赋值。
和UnsafePointer
一样,UnsafeRawPointer
同样不能直接创建,我们需要借助的它的可变形态UnsafeMutableRawPointer
来创建。
UnsafeMutableRawPointer
是 UnsafeRawPointer
的可变版本,但是它依然没有任何类型,也不确定任何值,我们创建的时候,需要给它指定占用内存的大小和对齐方式,这样就能形成一个完成的内存布局:
let usmrp = UnsafeMutableRawPointer.allocate(bytes: 4, alignedTo: 1)
接下来通过创建的 usmrp
创建一个UnsafeRawPointer
的不可变的指针:
let usrp = UnsafeRawPointer.init(usmrp)
print(usmrp)
print(usrp)
// 同一块内存
0x0000000101e1a570
0x0000000101e1a570
我们在一开始就说了,raw pointer
没有指定内存的类型,也没有初始化的值,他只是圈了一块内存,所以我们压根就不知道它指向的那个内存中到有什么。并且,它也不包含pointee
这个属性了。
虽然我们不知道raw pointer
指向的内存中有什么,但是我们可以通过raw pointer
的 advanced(by:)
函数获取之后的地址的块区。每一次调用advanced(by:)
都会返回一个地址。如果将之后的地址绑定某个类型之后,我们就可以在这个地址的片区中赋值了。
let newPtr = usrp.advanced(by: 16) // 这是往后偏移 16个字节之后的地址
需要进一步使用usrp
和usmrp
,应该给他们绑定类型。 如果我们需要绑定类型,可以使用Swift提供的两个函数:
public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafeMutablePointer<T> // 绑定一个类型,以及类型的大小
public func assumingMemoryBound<T>(to: T.Type) -> UnsafeMutablePointer<T>
// 重绑定一个类型
我们可以通过上述的两个函数来给一个指定的地址区域赋值:
newPtr. assumingMemoryBound(to: Int.self) // 将内存绑定为 Int 类型
newPtr.initialize(12) //初始化赋值
print(newPtr.pointee) // 12
这里可以看到,我并没有显示的指定需要的赋值的哪部分内存有多大,是因为当我们给内存绑定了类型的时候,系统会自动计算我想要是使用的内存区域大小(sizeOf(Int))。
不管是UnsafeRawPointer
还是UnsafeMutableRawPointer
都有上面两个方法,他们都会返回 typed pointer
, 分别和UnsafePointer
和UnsafeMutablePointer
对应。
经过绑定的raw pointer
就变成了typed pointer
,这时候的使用参考文中typed pointer
的内容即可。
2. UnsafeRawBufferPointer和UnsafeMutableRawBufferPointer
内存中的每个字节都被视为一个独立于内存绑定类型的 UInt8 值。 UnsafeRawBufferPointer
和UnsafeMutableRawBufferPointer
指代的是一系列的没有被绑定类型的内存区域。我们可以理解成他们实际上就是一些数组,再绑定内存之前,其中包含的元素则是每一个字节。
在底层,基本数据单元的复制是有效的,另外没有被 retain 和 stong 的也是能够安全的复制的,同样的,对于来自C API的对象也能够安全的复制。对于原声的Swift类型,有的包含了引用的对象的复制则有可能失败,但是我们可以使用指针对他们的值进行复制,这样的结果是有效的。如果我们强行对一下发类型进行复制,不一定有效,除非使用像C语言中的APImemmove().
来操作。
UnsafeRawBufferPointer
和UnsafeMutableRawBufferPointer
是内存视图,尽管我们知道它指向的内存区域,但是它并不拥有这块内存的引用。复制UnsafeRawBufferPointer
类型的变量不会复制它的内存;但是初始化一个集合到另一个新的集合过程会复制集合中的引用内存。
代码实现一下如何创建UnsafeRawBufferPointer
和UnsafeMutableRawBufferPointer
:
let usmrbp = UnsafeMutableRawBufferPointer.allocate(count: 3)
创建一个无类型的UnsafeMutableRawBufferPointer
指针。 之前说了,内存中每个字节都被视为一个独立于内存绑定类型的 UInt8 值,因此,这里的无类型实际上是有类型的,他应该属于 UInt8 类型。 而这个系列的大小应该为 3 * MemoryLayout<T>.stride 。
我们可以向该指针中的内存中赋值一个遵循 collect
的对象:
usmrbp.copyBytes(from: [5,56,6])
usmrbp.forEach { (a) in
print(a)
}
//
5
56
6
基本上, typed point
描述的是我们已经知道的哪部分内存,通过typed point
我们可以知道内存的地址,内存的类型,以及内存中存储的是什么。raw pointer
则描述的是一个内存的起始地址值,通过这个起始的地址,我们能够逐步的获取到后续的地址值,结合绑定类型之后, typed point
总能在使用一块合适地址保存我们想要保存的值。
不管是raw pointer
还是typed point
指针,两者都是不安全的,不安全的意义包含了内存的管理和对其他未知内存的侵害。 有时候使用指针无意改变了某些系统内部的关键变量,将会导致莫名其妙的崩溃。
三、如何正确的使用raw pointer
和typed point
我们自己创建的指针缺乏对内存的管理,因此非常脆弱,如何正确安全的使用指针值得我们探究。Swift 提供了好几种方式供我们用于指针和Swift原生类的交互。
1. Unmanaged 托管
因为直接使用指针需要我们去管理内存,这很繁琐,并且很危险。于是,Unmanaged 出现了。Unmanaged 能够将由 C API 传递过来的指针进行托管,我们可以通过Unmanaged标定它是否接受引用计数的分配,以便实现类似自动释放的效果;同时,如果不是使用引用计数,也可以使用Unmanaged 提供的release
函数来手动释放,这比在指针中进行这些操作要简单很多。
首先,假设我有一个 PersonClass
类型的对象,我想将它用一个指针表示:
class PersonClass{
let name: String = "hua"
let age: Int = 22
}
let um = Unmanaged.passUnretained(PersonClass() as AnyObject).toOpaque()
let tpeUns = um.bindMemory(to: Byte.self, capacity: MemoryLayout<Int>.stride)
print(UnsafeMutablePointer<Byte>(tpeUns))
//
0x0000000153e490e0
打印的地址则是这个对象的存储地址了,我们可以使用这个指针来表示这个对象。如果我们想取出和更改这个对象中的属性 name
和 age
的时候,可以这样操作:
let rptr = UnsafeMutableRawPointer(tpeUns) // 转化成不透明指针, 用于重绑定类型。
// 注意,再上一篇文章中已经介绍,Swift 中类的首地址有效值在 16 字节之后。
let namePtr = rptr.advanced(by: 16).assumingMemoryBound(to: String.self)
let agePtr = rptr.advanced(by: MemoryLayout<String>.stride + 16).assumingMemoryBound(to: Int.self)
print(namePtr.pointee,agePtr.pointee) // 取出内存值
print(ps.name,ps.age) // 打印原对象
//namePtr.pointee = "Tom - Loo1" //直接给指针赋值也是可以的。
namePtr.initialize(to: "Tom - Loo") // 重新初始化内存中的值
agePtr.initialize(to: 26)
print(ps.name,ps.age)
// 打印
Tom 22
Tom - Loo 26
总结起来,基本思路就是获取到了对象的地址(即首地址,注意这个地址应该使用不透明指针表示,以便后续根据属性的不同类型重新绑定),然后偏移16个字节,跳过 meta 信息所占的空间。根据属性的排布顺序,逐步计算取出每个属性所对应的指针,有了每个属性的指针,要赋值就简单了。 令人惊喜的是: 这里不管是let 属性还是 var 属性,都可以进行赋值的。
其实到这里,如何进行内存的基本思路已经出来了。 但是这篇文章主要是介绍指针的使用,我们继续看看另一种指针使用方式,用来获取一个属性而非整个对象的指针。
2. withPoint系列方法
Swift提供了一种更为简便的方式给我们获取一个属性的指针,那就是withPoint系列方法:
public func withUnsafeMutablePointer<T, Result>(to arg: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
public func withUnsafePointer<T, Result>(to arg: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
public func withExtendedLifetime<T, Result>(_ x: T, _ body: (T) throws -> Result) rethrows -> Result
public func withExtendedLifetime<T, Result>(_ x: T, _ body: () throws -> Result) rethrows -> Result
一般使用前面两个函数的概率更高,我重点说下这两个。
同样对于上述的PersonClass
类,如果修改它的属性值,我们可以如下操作:
withUnsafeMutablePointer(to: &ps.name) {
print($0.pointee)
$0.pointee = "Tom - hans"
}
print(ps.name)
这个函数如果使用在这里实际上并没有什么优势: 如果使用withUnsafeMutablePointer(to:) 方法修改属性值,必须保证属性的类型是 var ,虽然在内部我们可以使用初始化指针的方式强行改变,但是在调用方法时,编译器会提示需要加入一个 var 的变量,而不能是常量。 如此说,我们为什么不直接使用属性的点语法直接赋值呢?
本文对Swift的中指针介绍只是它的一部分功能,笔者能力有限,可能在文中出现纰漏和错误。欢迎指正。
网友评论