本文系學習Swift中的指針操作詳解的整理
默認情況下Swift是內存安全的,蘋果官方不鼓勵我們直接操作內存。但是,Swift中也提供了使用指針操作內存的方法,直接操作內存是很危險的行為,很容易就出現錯誤,因此官方將直接操作內存稱為 “unsafe 特性”。
在操作指針之前,需要理解幾個概念:size、alignment、stride,以及他們的獲取/使用,這就用到了 MemoryLayout :
MemoryLayout
使用MemoryLayout,可以檢測某個類型的實際大小(size),內存對齊大小(alignment),以及實際占用的內存大小(步長:stride),其單位均為字節;
public enum MemoryLayout<T> {
public static var size: Int { get }
public static var stride: Int { get }
public static var alignment: Int { get }
public static func size(ofValue value: T) -> Int
public static func stride(ofValue value: T) -> Int
public static func alignment(ofValue value: T) -> Int
}
例如:如果一個類型的大小(size)為5字節,對齊內存(alignment)大小為4字節,那么其實際占用的內存大小(stride)為8字節,這是因為編譯需要為其填充空白的邊界,使其符合它的 4 字節內存邊界對齊。
常見基本類型的內存size、alignment、stride:
MemoryLayout<Int>.size // return 8 (on 64-bit)
MemoryLayout<Int>.alignment // return 8 (on 64-bit)
MemoryLayout<Int>.stride // return 8 (on 64-bit)
MemoryLayout<Int16>.size // return 2
MemoryLayout<Int16>.alignment // return 2
MemoryLayout<Int16>.stride // return 2
MemoryLayout<Bool>.size // return 1
MemoryLayout<Bool>.alignment // return 1
MemoryLayout<Bool>.stride // return 1
MemoryLayout<Float>.size // return 4
MemoryLayout<Float>.alignment // return 4
MemoryLayout<Float>.stride // return 4
MemoryLayout<Double>.size // return 8
MemoryLayout<Double>.alignment // return 8
MemoryLayout<Double>.stride // return 8
原文中Bool的相關值為2,在Playground中測試結果為1,所以這里更改為了1
一般在移動指針的時候,對于特定類型,指針一次移動一個stride(步長),移動的范圍,要在分配的內存范圍內,切記超出分配的內存空間,切記超出分配的內存空間,切記超出分配的內存空間。
一般情況下stride是alignment的整數倍,即符合內存對齊原則;實際分配的內存空間大小也是alignment的整數倍,但是實際實例大小可能會小于實際分配的內存空間大小。
UnsafePointer
所有指針類型為 UnsafePointer,一旦你操作了內存,編譯器不會對這種操作進行檢測,你需要對自己的代碼承擔全部的責任。Swift中定義了一些特定類型的指針,每個類型都有他們的作用和目的,使用適當的指針類型可以防止錯誤的發生,并且更清晰得表達開發者的意圖,防止未定義行為的產生。
通過指針類型的名稱,我們可以知道這是一個什么類型的指針:可變/不可變、原生(raw)/有類型、是否是緩沖類型(buffer),大致有以下8種類型:
Pointer Name | Unsafe? | Write Access? | Collection | Strideable? | Typed? |
UnsafeMutablePointer<T> | yes | yes | no | yes | yes |
UnsafePointer<T> | yes | no | no | yes | yes |
UnsafeMutableBufferPointer<T> | yes | yes | yes | no | yes |
UnsafeBufferPointer<T> | yes | no | yes | no | yes |
UnsafeRawPointer | yes | no | no | yes | no |
UnsafeMutableRawPointer | yes | yes | no | yes | no |
UnsafeMutableRawBufferPointer | yes | yes | yes | no | no |
UnsafeRawBufferPointer | yes | no | yes | no | no |
- unsafe:不安全的
- Write Access:可寫入
- Collection:像一個容器,可添加數據
- Strideable:指針可使用 advanced 函數移動
- Typed:是否需要指定類型(范型)
原生(Raw)指針
// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let aligment = MemoryLayout<Int>.alignment
let byteCount = stride * count
// 2
do {
print("raw pointers")
// 3
let pointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: aligment)
// 4
defer {
pointer.deallocate()
}
// 5
pointer.storeBytes(of: 42, as: Int.self)
pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
// 讀取第一個值
pointer.load(as: Int.self)
// 讀取第二個值
pointer.advanced(by: stride).load(as: Int.self)
// 6
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
for (index, byte) in bufferPointer.enumerated() {
print("bute \(index): \(byte)")
}
}
代碼說明:
- 聲明示例用到的參數
count :整數的個數
stride:整數的步長
aligment:整數的內存對齊大小
byteCount:實際需要的內存大小
- 聲明示例用到的參數
- 聲明作用域
使用 do 來增加一個作用域,讓我們可以在接下的示例中復用作用域中的變量名。
- 聲明作用域
- UnsafeMutableRawPointer.allocate 創建分配所需字節數,該指針可以用來讀取和存儲(改變)原生的字節。
這里分配內存使用了allocate 方法:
public static func allocate(byteCount: Int, alignment: Int) -> UnsafeMutableRawPointer
- byteCount:所需字節數
- alignment:內存對齊
- 延時釋放
使用 defer 來保證內存得到正確地釋放,操作指針的時候,所有內存都需要我們手動進行管理。
這里釋放內存使用了deallocate方法:
- 延時釋放
public func deallocate()
allocate 和 deallocate 方法一定要配對出現。
- 使用 storeBytes 和 load 方法存儲和讀取字節
存儲數據方法storeBytes:
/// - Parameters:
/// - value: The value to store as raw bytes.
/// - offset: The offset from this pointer, in bytes. `offset` must be
/// nonnegative. The default is zero.
/// - type: The type of `value`.
public func storeBytes<T>(of value: T, toByteOffset offset: Int = default, as type: T.Type)
- value:要存儲的值
- offset:偏移量,默認即可
- type:值的類型
讀取數據方法 load :
/// - Parameters:
/// - offset: The offset from this pointer, in bytes. `offset` must be
/// nonnegative. The default is zero.
/// - type: The type of the instance to create.
/// - Returns: A new instance of type `T`, read from the raw bytes at
/// `offset`. The returned instance is memory-managed and unassociated
/// with the value in the memory referenced by this pointer.
public func load<T>(fromByteOffset offset: Int = default, as type: T.Type) -> T
- offset:偏移量,默認即可
- type:值的類型
移動指針地址 advanced :
/// - Parameter n: The number of bytes to offset this pointer. `n` may be
/// positive, negative, or zero.
/// - Returns: A pointer offset from this pointer by `n` bytes.
public func advanced(by n: Int) -> UnsafeMutableRawPointer
- n:步長stride
使用原生指針,存儲下一個值的時候需要移動一個步長(stride),也可以直接使用 + 運算符:
(pointer + stride).storeBytes(of: 6, as: Int.self)
- UnsafeRawBufferPointer 類型以字節流的形式來讀取內存。這意味著我們可以這些字節進行迭代,對其使用下標,或者使用 filter,map 以及 reduce 這些很酷的方法,緩沖類型指針使用了原生指針進行初始化。
類型指針
do {
print("Typed pointers")
// 1.
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count)
// 2.
defer {
pointer.deinitialize(count: count)
pointer.deallocate()
}
// 3.
pointer.pointee = 42
pointer.advanced(by: 1).pointee = 6
pointer.pointee
pointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value \(index): \(value)")
}
}
類型指針與原生指針的區別,主要體現在上面標注數字的幾個地方:
- 分配內存、初始化
類型指針,在分配內存的時候通過給范型賦值來指定當前指針所操作的數據類型:
/// - Parameter count: The amount of memory to allocate, counted in instances
/// of `Pointee`.
public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
- count:要存儲的數據個數
可以看到其分配內存的方法,只有一個參數,指定所要存儲的數據個數即可,因為通過給范型參數賦值,已經知道了要存儲的數據類型,其alignment和stride就確定了,這時只需要再知道存儲幾個數據即可。
這里還多了個初始化的過程,類型指針單單分配內存,還不能使用,還需要初始化:
/// - Parameters:
/// - repeatedValue: The instance to initialize this pointer's memory with.
/// - count: The number of consecutive copies of `newValue` to initialize.
/// `count` must not be negative.
public func initialize(repeating repeatedValue: Pointee, count: Int)
- repeatedValue:默認值
- count:數量
- 延時釋放
在釋放的時候,要先釋放已初始化的實例(deinitialize),再釋放已分配的內存(deallocate)空間:
/// - Parameter count: The number of instances to deinitialize. `count` must
/// not be negative.
/// - Returns: A raw pointer to the same address as this pointer. The memory
/// referenced by the returned raw pointer is still bound to `Pointee`.
public func deinitialize(count: Int) -> UnsafeMutableRawPointer
- count:數量
- 存儲/讀取
類型指針的存儲/讀取值,不需要再使用storeBytes/load,Swift提供了一個以類型安全的方式讀取和存儲值--pointee:
- 存儲/讀取
public var pointee: Pointee { get nonmutating set }
這里的移動指針的方法,和上面的一致,也是 advanced ,但是其參數有所不同:
/// - Parameter n: The number of strides of the pointer's `Pointee` type to
/// offset this pointer. To access the stride, use
/// `MemoryLayout<Pointee>.stride`. `n` may be positive, negative, or
/// zero.
/// - Returns: A pointer offset from this pointer by `n` instances of the
/// `Pointee` type.
public func advanced(by n: Int) -> UnsafeMutablePointer<Pointee>
- n:這里是按類型值的個數進行移動
同樣,這里也可以使用運算符 + 進行移動:
(pointer + 1).pointee = 6
原生指針轉換為類型指針
do {
print("Converting raw pointers to typed pointers")
// 創建原生指針
let rawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: aligment)
// 延遲釋放原生指針的內存
defer {
rawPointer.deallocate()
}
// 將原生指針綁定類型
let typePointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typePointer.initialize(repeating: 0, count: count)
defer {
typePointer.deinitialize(count: count)
}
typePointer.pointee = 42
typePointer.advanced(by: 1).pointee = 9
typePointer.pointee
typePointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: typePointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value \(index): \(value)")
}
}
原生指針轉換為類型指針,是通過調用內存綁定到特定的類型來完成的:
/// - Parameters:
/// - type: The type `T` to bind the memory to.
/// - count: The amount of memory to bind to type `T`, counted as instances
/// of `T`.
/// - Returns: A typed pointer to the newly bound memory. The memory in this
/// region is bound to `T`, but has not been modified in any other way.
/// The number of bytes in this region is
/// `count * MemoryLayout<T>.stride`.
public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafeMutablePointer<T>
- type:數據類型
- count:容量
通過對內存的綁定,我們可以通過類型安全的方法來訪問它。其實我們手動創建類型指針的時候,系統自動幫我們進行了內存綁定。
獲取一個實例的字節
這里定義了一個結構體 Sample來作為示例:
struct Sample {
var number: Int
var flag: Bool
init(number: Int, flag: Bool) {
self.number = number
self.flag = flag
}
}
do {
print("Getting the bytes of an instance")
var sample = Sample(number: 25, flag: true)
// 1.
withUnsafeBytes(of: &sample) { (rs) in
for bute in rs {
print(bute)
}
}
}
這里主要是使用了withUnsafeBytes 方法來實現獲取字節數:
/// - Parameters:
/// - arg: An instance to temporarily access through a raw buffer pointer.
/// - body: A closure that takes a raw buffer pointer to the bytes of `arg`
/// as its sole argument. If the closure has a return value, that value is
/// also used as the return value of the `withUnsafeBytes(of:_:)`
/// function. The buffer pointer argument is valid only for the duration
/// of the closure's execution.
/// - Returns: The return value, if any, of the `body` closure.
public func withUnsafeBytes<T, Result>(of arg: inout T, _ body: (UnsafeRawBufferPointer) throws -> Result) rethrows -> Result
- arg:實例對象地址
- body:回調閉包,參數為UnsafeRawBufferPointer 類型的指針
注意:該方法和回調閉包都有返回值,如果閉包有返回值,此返回值將會作為該方法的返回值;但是,一定不要在閉包中將body的參數,即:UnsafeRawBufferPointer 類型的指針作為返回值返回,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包。
withUnsafeBytes 同樣適合用 Array 和 Data 的實例.
使用指針的原則
不要從 withUnsafeBytes 中返回指針
絕對不要讓指針逃出 withUnsafeBytes(of:) 的作用域范圍。這樣的代碼會成為定時炸彈,你永遠不知道它什么時候可以用,而什么時候會崩潰。
一次只綁定一種類型
在使用 bindMemory方法將原生指針綁定內存類型,轉為類型指針的時候,一次只能綁定一個類型,例如:將一個原生指針綁定Int類型,不能再綁定Bool類型:
let typePointer = rawPointer.bindMemory(to: Int.self, capacity: count)
// 一定不要這么做
let typePointer1 = rawPointer.bindMemory(to: Bool.self, capacity: count)
但是,我們可以使用 withMemoryRebound 來對內存進行重新綁定。并且,這條規則也表明了不要將一個基本類型(如 Int)重新綁定到一個自定義類型(如 class)上。
/// - Parameters:
/// - type: The type to temporarily bind the memory referenced by this
/// pointer. The type `T` must be the same size and be layout compatible
/// with the pointer's `Pointee` type.
/// - count: The number of instances of `T` to bind to `type`.
/// - body: A closure that takes a mutable typed pointer to the
/// same memory as this pointer, only bound to type `T`. The closure's
/// pointer argument is valid only for the duration of the closure's
/// execution. If `body` has a return value, that value is also used as
/// the return value for the `withMemoryRebound(to:capacity:_:)` method.
/// - Returns: The return value, if any, of the `body` closure parameter.
public func withMemoryRebound<T, Result>(to type: T.Type, capacity count: Int, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
- type:值的類型
- count:值的個數
- body:回調閉包,參數為UnsafeRawBufferPointer 類型的指針
注意:該方法和回調閉包都有返回值,如果閉包有返回值,此返回值將會作為該方法的返回值;但是,一定不要在閉包中將body的參數,即:UnsafeRawBufferPointer 類型的指針作為返回值返回,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包,該參數的使用范圍僅限當前閉包。
不要操作超出范圍的內存
do {
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
// 1. 這里的count+1,超出了原有指針pointer分配的內存范圍
let bufferPointer = UnsafeRawBufferPointer.init(start: pointer, count: count + 1)
for byte in bufferPointer {
print(byte)
}
}
這里的count+1,超出了原有指針pointer分配的內存范圍,切記不要出現這種情況。