引言
一般来说,Swift是一门非常安全的语言,平时做业务的时候,基本上用的结构体,类等,都不会直接操作内存,而且有可选值的包装,也保证了值的安全。但是也有一些比较特殊的业务需要用到指针,比如与底层C的交互,字节流的解析等。这时候,Swift将会变得没那么安全,需要我们小心翼翼的操作,并且需要深刻理解Swift的指针类型
MemoryLayout
在讲指针之前,必须先得了解一下Swift的内存布局MemoryLayout,不同的基础类型、结构体、枚举、类等等,他们在内存中占有的大小,步长,对齐长度都不一样。一旦指针的偏移量、获取值的长度错误,轻则获取的值错误,重则程序崩溃。
基础类型的MemoryLayout
MemoryLayout<Int>.size // returns 8 (on 64-bit)
MemoryLayout<Int>.alignment // returns 8 (on 64-bit)
MemoryLayout<Int>.stride // returns 8 (on 64-bit)
MemoryLayout<Int16>.size // returns 2
MemoryLayout<Int16>.alignment // returns 2
MemoryLayout<Int16>.stride // returns 2
MemoryLayout<Bool>.size // returns 1
MemoryLayout<Bool>.alignment // returns 1
MemoryLayout<Bool>.stride // returns 1
MemoryLayout<Float>.size // returns 4
MemoryLayout<Float>.alignment // returns 4
MemoryLayout<Float>.stride // returns 4
MemoryLayout<Double>.size // returns 8
MemoryLayout<Double>.alignment // returns 8
MemoryLayout<Double>.stride // returns 8
...
MemoryLayout是一个在编译时评估的泛型类型。它确定每个指定类型的大小、对齐方式和步长,并返回一个以字节为单位的数字。
我们可以靠如上的方式获取数据类型的MemoryLayout:
size:数据类型的长度,就是数据在内存中占据的大小alignment:数据类型的对齐方式,在某些结构中(比如在Raw Pointer),数据在内存的首地址必须是alignment的倍数,否则将会崩溃,例如Int16的alignment是2,那么Int16在内存的首地址必须是偶数,否则奔溃stride:数据类型的步长,应该是alignment的倍数,如果是一串紧挨的数据,那么下一个数据会在大于等于stride的地址之后
关于数据类型的对齐方式举个例子,Demo如下:
//向堆申请开辟空间
let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)
//结束时释放空间
defer {
p.deallocate()
}
//在这块连续的内存空间内,放上0,1,2,3
for i in 0..<4 {
p.advanced(by: i * 8).storeBytes(of: i, as: Int.self)
}
let offset = 8
print(p.advanced(by: offset).load(as: Int.self))
Int的alignment为8,当offset为8的倍数时,可以取到对应的值,即使越界,也能取到0或者奇怪的数字,不会崩溃。但如果offset不为8的倍数时,程序直接飘红:
将会出现:
Fatal error: load from misaligned raw pointer
翻译过来就是:
致命错误:从未对齐的原始指针加载
其他类型的MemoryLayout
Swift中还有许多其他的类型:结构体,枚举,类等
我们可以用同样的方式来查看他们的MemoryLayout
struct EmptyStruct {}
MemoryLayout<EmptyStruct>.size // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride // returns 1
struct SampleStruct {
let number: UInt32
let flag: Bool
}
MemoryLayout<SampleStruct>.size // returns 5
MemoryLayout<SampleStruct>.alignment // returns 4
MemoryLayout<SampleStruct>.stride // returns 8
class EmptyClass {}
MemoryLayout<EmptyClass>.size // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)
class SampleClass {
let number: Int64 = 0
let flag = false
}
MemoryLayout<SampleClass>.size // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)
我们可以发现:
EmptyStruct空结构体的大小为零。对齐大小是1,所有数字都可以被1整除,所以它可以存放于任何地址。步长也为1,虽然它的大小为零,但是必须有一个唯一的内存地址。
SampleStruct的大小为5,等于UInt32和Bool大小的相加。对齐大小是4,等于UInt32的对齐大小,属性中最大的对齐方式会决定结构体的对齐方式。步长为8,比5大的,最小的4的倍数为8,所以步长为8。
EmptyClass和SampleClass,不管是否包含属性,他们的大小,对齐方式,步长都为8,因为类是引用类型,本质上就是一个指针,在64位的系统下,指针长度就是为8.
当然,还有枚举、数组、字典等等的MemoryLayout,以及结构体属性内部顺序不一样,会造成结构体的大小也不一样,这里就不详细说明了,感兴趣的可以自己实验下,有时间我会单独开一篇文章详述。
指针类型
在Swift中,因为指针可以直接操作内存,所以用上了Unsafe的开头修饰,虽然每次申明指针的时候,要多写这个修饰词,但是可以时刻提醒着你在访问编译器没有检查的内存,如果操作不当,你会遇到一些奇怪的问题,甚至奔溃。
Swift不像C的char *那样只提供一种非结构化方式访问内存的非安全指针类型。Swift包含近12种指针类型,每种类型都有不同的功能和用途。
我们看下常用的8个指针类型:
| 指针类型 | Editable | Collection | Strideable | Typed |
|---|---|---|---|---|
UnsafeMutablePointer<T> | YES | NO | YES | YES |
UnsafePointer<T> | NO | NO | YES | YES |
UnsafeMutableBufferPointer<T> | YES | YES | NO | YES |
UnsafeBufferPointer<T> | NO | YES | NO | YES |
UnsafeMutableRawPointer | YES | NO | YES | NO |
UnsafeRawPointer | NO | NO | YES | NO |
UnsafeMutableRawBufferPointer | YES | YES | NO | NO |
UnsafeRawBufferPointer | NO | YES | NO | NO |
Editable: 类型名中带有Mutable的,说明都是可以写入更改数据的,其余的只能读取,并不能修改,可以保护数据安全。根据自己的需求看,是否需要MutableCollection: 类型名中带有Buffer的,都具有Collection的特性,如果查看他们的声明文件,发现他们都遵守Collection协议Strideable: 不带有Buffer的可以调用advanced方法,Typed类型的默认步长为T的步长,而Raw类型的默认步长为1Typed: 有范型的是Typed Pointer,带有Raw字段的是Raw Pointer
不同的类型会用在不同的场景,看情况使用,或者相互转换
指针的操作
我们可以在堆上开辟一块空间,然后用指针指向它。在调用初始化方法的时候,指针必须是Mutable类型的,不然没有意义。而且指针是Unsafe的,编译器不会参与该指针的生命周期,所以,自己在堆上申请空间后,结束使用时必须要自己释放,不然会内存泄露。
Unsafe Pointer初始化
//像堆内存申请开辟空间
var mutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
//作用域结束时执行
defer {
//释放开辟的空间
mutableRawPointer.deallocate()
}
var mutableTypedPointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
mutableTypedPointer.initialize(repeating: 0, count: count)
defer {
mutableTypedPointer.deinitialize(count: count)
mutableTypedPointer.deallocate()
}
var mutableRawBufferPointer = UnsafeMutableRawBufferPointer.allocate(byteCount: byteCount, alignment: alignment)
defer {
mutableRawBufferPointer.deallocate()
}
var mutableTypedBufferPointer = UnsafeMutableBufferPointer<Int>.allocate(capacity: count)
mutableTypedBufferPointer.initialize(repeating: 0)
defer {
mutableTypedBufferPointer.deallocate()
}
Unsafe Pointer赋值与取值操作
//mutableRawPointer的存值与取值,mutableRawPointer默认步长为1
(mutableRawPointer + 2 * alignment).storeBytes(of: 23, as: Int.self)
print(mutableRawPointer.advanced(by: 2 * alignment).load(as: Int.self))
mutableRawPointer.storeBytes(of: 33, toByteOffset: 2 * alignment, as: Int.self)
print(mutableRawPointer.load(fromByteOffset: 2 * alignment, as: Int.self))
//mutableTypedPointer的存值与取值,mutableRawPointer默认步长为Int的步长
mutableTypedPointer.advanced(by: 2).pointee = 27
print(mutableTypedPointer[2])
(mutableTypedPointer + 2).pointee = 37
print(mutableTypedPointer.successor().successor().pointee)
//mutableRawBufferPointer的存值与取值,可以像数组一样取单个元素,不过是UInt8的
mutableRawBufferPointer.storeBytes(of: 13, toByteOffset: 2 * alignment, as: Int.self)
print(mutableRawBufferPointer.load(fromByteOffset: 2 * alignment, as: Int.self))
//发现内存中有垃圾值,但是找不到初始化成0的便利方法,好奇怪
for (index, value) in mutableRawBufferPointer.enumerated() {
print("value \(index): \(value)")
}
//mutableTypedBufferPointer的存值与取值,和mutableRawBufferPointer看成一个数组,不过mutableRawBufferPointer是[UInt8],而mutableTypedBufferPointer是[Int]
mutableTypedBufferPointer[2] = 9
print(mutableTypedBufferPointer[2])
for (index, value) in mutableTypedBufferPointer.enumerated() {
print("value \(index): \(value)")
}
同类型mutable互转
var rawPointer = UnsafeRawPointer(mutableRawPointer)
var typedPointer = UnsafePointer(mutableTypedPointer)
var rawBufferPointer = UnsafeRawBufferPointer(mutableRawBufferPointer)
var typedBufferPointer = UnsafeBufferPointer(mutableTypedBufferPointer)
mutableRawPointer = UnsafeMutableRawPointer(mutating: rawPointer)
mutableTypedPointer = UnsafeMutablePointer(mutating: typedPointer)
mutableRawBufferPointer = UnsafeMutableRawBufferPointer(mutating: rawBufferPointer)
mutableTypedBufferPointer = UnsafeMutableBufferPointer(mutating: typedBufferPointer)
在OC中的可变到不可变,往往意味着深拷贝。那么在这里的指针会不会拷贝一份内容到新的指针么?
我们可以打印下他们的地址:
我们可以很明显看到,他们的地址完全没变,换句话说,他们都指向同一片内存地址。所以mutable这个修饰词只是在API层面限制你能不能修改内存而已。
不同类型的指针相互转化
/**
这块转化会引起上方defer中释放的崩溃,因为对象变了,同一块空间多次释放了,这边只是示范转化,真的要运行程序,请注释这块
*/
//其他类型转mutableRawPointer
mutableRawPointer = UnsafeMutableRawPointer(mutableTypedPointer)
//如果你能保证count不为0,可以强转。但是如果baseAddress有值,不一定count大于0。在底层,UnsafePointer<T> 和 Optional<UnsafePointer<T> 的内存结构完全相同;编译器会将 Optional.none 映射为一个所有位全为零的空指针。
mutableRawPointer = mutableRawBufferPointer.baseAddress!
mutableRawPointer = UnsafeMutableRawPointer(mutableTypedBufferPointer.baseAddress!)
//其他类型转mutableTypedPointer
mutableTypedPointer = mutableRawPointer.bindMemory(to: Int.self, capacity: count)
mutableTypedPointer = mutableRawPointer.assumingMemoryBound(to: Int.self)
mutableTypedPointer = mutableRawBufferPointer.baseAddress!.assumingMemoryBound(to: Int.self)
mutableTypedPointer = mutableTypedBufferPointer.baseAddress!
//其他类型转mutableRawBufferPointer
mutableRawBufferPointer = UnsafeMutableRawBufferPointer.init(start: mutableRawPointer, count: byteCount)
mutableRawBufferPointer = UnsafeMutableRawBufferPointer.init(start: UnsafeMutableRawPointer(mutableTypedPointer), count: byteCount)
mutableRawBufferPointer = UnsafeMutableRawBufferPointer(mutableTypedBufferPointer)
//其他类型转mutableTypedBufferPointer
mutableTypedBufferPointer = UnsafeMutableBufferPointer.init(start: mutableRawPointer.assumingMemoryBound(to: Int.self), count: count)
mutableTypedBufferPointer = UnsafeMutableBufferPointer.init(start: mutableTypedPointer, count: count)
mutableTypedBufferPointer = mutableRawBufferPointer.bindMemory(to: Int.self)
mutableTypedBufferPointer = UnsafeMutableBufferPointer.init(start: mutableRawBufferPointer.baseAddress?.assumingMemoryBound(to: Int.self), count: mutableRawBufferPointer.count/MemoryLayout<Int>.stride)
//终极转化方法(buffer不建议用,至少count就不一样),比较危险,除非确保正确,类的引用也可以靠这个变成指针
mutableRawPointer = unsafeBitCast(mutableTypedPointer, to: UnsafeMutableRawPointer.self)
mutableTypedPointer = unsafeBitCast(mutableRawPointer, to: UnsafeMutablePointer<Int>.self)
这边的转化方法应该不全,如果好的方法遗漏的,欢迎评论区补充。
bindMemory和assumingMemoryBound的区别
先贴下官方注释
bindMemory:
Binds the memory to the specified type and returns a typed pointer to the bound memory.
Use the
bindMemory(to:capacity:)method to bind the memory referenced by this pointer to the typeT. The memory must be uninitialized or initialized to a type that is layout compatible withT. If the memory is uninitialized, it is still uninitialized after being bound toT.
assumingMemoryBound:
Returns a typed pointer to the memory referenced by this pointer, assuming that the memory is already bound to the specified type.
Use this method when you have a raw pointer to memory that has already been bound to the specified type. The memory starting at this pointer must be bound to the type
T. Accessing memory through the returned pointer is undefined if the memory has not been bound toT. To bind memory toT, usebindMemory(to:capacity:)instead of this method.
这里先说下几个状态
Uninitialised raw memory: 未初始化的原始内存Uninitialised memory that's bound to a type: 绑定到类型的未初始化内存Initialised memory bound to a type: 绑定到类型的初始化内存
当你使用UnsafeMutableRawPointer.allocate(bytes:alignment:)分配内存时,你得到的是未初始化的原始内存。
当你使用UnsafeMutablePointer<T>.allocate(capacity:)分配内存时,你会得到绑定到类型T的未初始化内存。
当你使用上述指针调用initialize系列方法可以得到绑定到类型T的初始化内存
如果指针的Pointee类型 (也就是指针指向的数据类型) 是一个需要内存管理的非简单类型 (例如在一个类或结构体中包含了其它类),你必须在调用deallocate之前调用deinitialize。initialize和deinitialize方法用于管理ARC中的引用计数。忘记调用deinitialize 可能会引起内存泄漏。更糟的是,如果忘记调用initialize,例如直接用下标操作符给指向一片未初始化内存的指针赋值,可以引发各种未定义的问题甚至让程序崩溃。
如果指针的Pointee类型是一个简单类型(例如结构体,枚举等),可以清除内存中的垃圾值,以免引起不可预料的错误,这个和C初始化内存很像。
我们看了官方的解释,可以简单理解为:
- 如果你的这块内存是未初始化的原始内存,调用
bindMemory后,将会得到绑定到类型的未初始化内存 - 如果你的这块内存是绑定到类型的初始化内存,但你想要绑定的类型和已绑定的类型的布局兼容,那么调用
bindMemory后,将会得到新绑定到类型的初始化内存 bindMemory只能上述两种情况下使用,其他情况可以调用assumingMemoryBound(包括上述的两种情况也可以调用,不过在返回未初始化的原始内存时,会出现未定义的指针,= =不懂啥意思,哈哈,评论区请指教)。
这里区分下相关类型和布局兼容类型是独立的概念:
- 布局兼容:如果绑定到类型
U的内存可以按位重新解释为具有类型T,则类型T在布局上与类型U兼容。注意,这并不一定是一种双向关系。例如,如果讲元祖(Int, Int)的一个'实例'可以被重新解释为2*Int,则Int与(Int, Int)布局兼容。但你不能用一个Int来构成一个(Int, Int)值。 - 相关类型:如果可以用这两种类型为重叠内存别名,那么这两种类型是相关的。例如,如果你有一个UnsafePointer和一个UnsafePointer,如果T和U是不相关的类型,那么它们就不能指向彼此重叠的内存。
我们看下源码的区别
@_transparent
@discardableResult
public func bindMemory<T>(
to type: T.Type, capacity count: Int
) -> UnsafePointer<T> {
Builtin.bindMemory(_rawValue, count._builtinWordValue, type)
return UnsafePointer<T>(_rawValue)
}
@_transparent
public func assumingMemoryBound<T>(to: T.Type) -> UnsafePointer<T> {
return UnsafePointer<T>(_rawValue)
}
@_transparent:和@inline(__always)非常类似,可以理解成内联函数,编译的时候,直接将函数体拷贝到调用的地方,有点像宏定义,这样不用开辟新的栈,效率提高了。@discardableResult:顾名思义,函数的返回结果可以不使用。如果不加这个关键词,那么你不使用该返回值时,编译器会飘黄提醒。Builtin:是内置命令,可以先了解下Swift的编译过程,Builtin将LLVM IR的类型和方法直接暴露给Swift标准库,Builtin模块只有在标准库内部才可以访问。
我们可以看到bindMemory比assumingMemoryBound只多了调用一个Builtin.bindMemory(_rawValue, count._builtinWordValue, type),猜测底层做了某些操作(也许没有哈),下次有机会分析LLVM源码,这块补上。bindMemory有个@discardableResult词修饰,说明这个方法更倾向于绑定类型,并不关注结果,只是顺带返回给你。而assumingMemoryBound真的只是从API层面将一段内存空间,变成你想绑定的类型操作。个人推荐,除非你真正想绑定内存类型,否则还是assumingMemoryBound用的情况多一点。
不过个人目前测下来,两者方法暂时没有区别。。。如果大佬有知道的,请告知。我查到资料在Swift3中的确是没有区别的,当时可能是ABI稳定的问题,现在就不知道了。不过还是建议听从官方的使用方法,谁知道以后会怎么样呢,也许更新后就改了呢,是吧。
Swift中的常见类型转指针
结构体
结构体是Swift的基础类型,像Int、Double、Bool等,都是结构体。下面举个列子:
struct Teacher {
var age = 12
var name = "Tom"
}
var tc = Teacher.init()
withUnsafePointer(to: &tc) { pointer in
// let pointer: UnsafePointer<Teacher>
print("name: \(pointer.pointee.name), age: \(pointer.pointee.age)")
// name: Tom, age: 12
}
// 可以不加取地址符号,效果一样
withUnsafeMutablePointer(to: tc) { pointer in
// let pointer: UnsafeMutablePointer<Teacher>
pointer.pointee.age = 15
pointer.pointee.name = "Harry"
print("name: \(pointer.pointee.name), age: \(pointer.pointee.age)")
// name: Harry, age: 15
}
withUnsafeBytes(of: &tc) { pointer in
// let pointer: UnsafeRawBufferPointer
print("data count: \(pointer.count)")
// data count: 24
print("name: \(pointer.load(as: Int.self)), age: \(pointer.load(fromByteOffset: MemoryLayout<Int>.stride, as: String.self))")
// name: 15, age: Harry 可以看到这里的值被上面已经改掉了,印证操作的同一片内存空间
}
withUnsafeMutableBytes(of: &tc) { pointer in
// let pointer: UnsafeMutableRawBufferPointer
pointer.storeBytes(of: 18, as: Int.self)
pointer.storeBytes(of: "Marry", toByteOffset: MemoryLayout<Int>.stride, as: String.self)
print("data count: \(pointer.count)")
// data count: 24
print("name: \(pointer.load(as: Int.self)), age: \(pointer.load(fromByteOffset: MemoryLayout<Int>.stride, as: String.self))")
// name: 18, age: Marry
}
系统提供了4种方法获取值类型的指针,根据自己需要,选择合适的方法,如果你不想改变实例的值,建议不要使用mutable的方法
类指针
类是引用类型,本身是一个指针,我们可以转成一个UnsafeRawPointer指针,但我们分析过类的结构,那我们可以声明一个大致的结构体HeapObject,将类转成UnsafePointer<HeapObject>类型,下面看下Demo:
struct HeapObject {
var kind: Int
var unownedRef: UInt32
var strongref: UInt32
}
class Teacher {
var age = 18
}
var t = Teacher()
// 我们可以不断声明变量使strongref增加
var t1 = t
do { // 可以使用Unmanaged获取指针,let ptr: UnsafeMutableRawPointer
let ptr = Unmanaged.passUnretained(t).toOpaque()
print(ptr.assumingMemoryBound(to: HeapObject.self).pointee)
// HeapObject(kind: 4295000432, unownedRef: 3, strongref: 2)
}
do { // 直接强转,因为我们确定结构是一致的
let ptr = unsafeBitCast(t, to: UnsafePointer<HeapObject>.self)
print(ptr.pointee)
// HeapObject(kind: 4295000432, unownedRef: 3, strongref: 2)
}
不能使用withUnsafePointer方法获取类的指向,因为你不管用不用取地址符,都拿到的是指针的指针(多态方法做处理了),并不是我们想要的。所以可以采用上面两种方法获取指针。
其余不常见的指针
因为很不常见,所以就一笔带过了
OpaquePointer:一个不透明的C指针的包装,用于表示不能用Swift表示的类型的C指针,例如不完整的struct类型。ManagedBufferPointer:包含一个buffer对象,并提供对Header实例,以及对存储在该缓冲区中的任意数量的Element实例的连续存储的访问。在大多数情况下,ManagedBuffer类可以很好地满足这个目的,并且可以单独使用。然而,在不同类的对象必须用作存储的情况下,就需要ManagedBufferPointer。AutoreleasingUnsafeMutablePointer:一个可变指针,指向一个不拥有目标的Objective-C引用,Pointee必须是一个类类型或Optional<C>,其中C是一个类。该类型有隐式转换,允许将以下任何一种传递给C或ObjC的API:nil,它作为一个空指针传递。- 引用类型的
inout参数,它作为一个指针传递给一个回写临时对象,该临时对象具有自动释放所有权语义。 - '
UnsafeMutablePointer<Pointee>,按原样传递。
CVaListPointer:空结构体,不知道啥作用。。