- 原文博客地址: 淺談Swift的內存管理
- 2020年的第一篇博客, 算是2020年開了個好頭, 希望能夠繼續堅持下去, 繼續記錄分享更多更高質量的文章
- 今年期待已久的
Swift5.0
穩定版就已經發布了, 感興趣的小夥伴可看我的這篇博客:Swift 5.0新特性更新 - 這篇博客可主要分享
Swift
的內存管理的相關介紹和剖析, 測試環境:Xcode 11.2.1
,Swift 5.1.2
內存管理
- 和
OC
一樣, 在Swift
中也是採用基於引用計數的ARC
內存管理方案(針對堆空間的內存管理) - 在
Swift
的ARC
中有三種引用- 強引用(
strong reference
):默認情況下,代碼中涉及到的引用都是強引用 - 弱引用(
weak reference
):通過weak
定義弱引用 - 無主引用(
unowned reference
):通過unowned
定義無主引用
- 強引用(
weak
- 弱引用(
weak reference
):通過weak
定義弱引用- 必須是可選類型的
var
,因爲實例銷燬後,ARC
會自動將弱引用設置爲nil
ARC
自動給弱引用設置nil
時,不會觸發屬性觀察
- 必須是可選類型的
- 在介紹
weak
弱引用之前, 先看一下下面一段代碼
class Animal {
deinit {
print("Animal deinit")
}
}
func test() {
let animal = Animal()
}
print("will deinit")
test()
print("did deinit")
上面這段代碼中在test
函數調用結束之後, 該作用的內存就會被回收, animal
對象自然就會被銷燬, 毫無疑問上面的輸出結果應該是
will deinit
Animal deinit
did deinit
同樣下面這段代碼, 同樣也是在a1
對象被置爲nil
的時候內存會被回收, 對象就會被銷燬
var a1: Animal? = Animal()
print("will deinit")
a1 = nil
print("did deinit")
- 下面是一個被
weak
修飾的弱引用對象, - 我們都知道, 被
weak
修飾的弱引用對象, 在對象銷燬的時候, 會被自動置爲nil
- 所以被
weak
修飾的弱引用對象必須是可選類型的var
, 兩個條件缺一不可
weak var a2: Animal? = Animal()
// 以下兩種方式都會報錯的
weak var a2: Animal = Animal()
weak let a2: Animal? = Animal()
unowned
- 無主引用(
unowned reference
):通過unowned
定義無主引用 - 不會產生強引用,實例銷燬後仍然存儲着實例的內存地址(類似於OC中的
unsafe_unretained
) - 試圖在實例銷燬後訪問無主引用,會產生運行時錯誤(如下野指針)
Fatal error: Attempted to read an unowned reference but object 0x0 was already deallocate
另外需要注意的是, weak
、unowned
只能用在類實例上面, 如下所示
// 該協議表示只能被類遵守, AnyObject代表所有的類實例
protocol Liveable: AnyObject {}
class Person {}
weak var p0: Person?
weak var p1: AnyObject?
// 所有能遵循Liveable協議的肯定都是類
weak var p2: Liveable?
unowned var p10: Person?
unowned var p11: AnyObject?
unowned var p12: Liveable?
循環引用
weak
、unowned
都能解決循環引用的問題,unowned
要比weak
少一些性能消耗- 在生命週期中可能會變爲
nil
的使用weak
- 初始化賦值後再也不會變爲
nil
的使用unowne
- 說道循環引用就自然想到了閉包
閉包的循環引用
閉包表達式默認會對用到的外層對象產生額外的強引用(對外層對象進行了retain
操作), 看一下下面的代碼中deinit
會被調用嗎?
class Person {
var fn: (() -> ())?
func run() { print("run") }
deinit { print("deinit") }
}
func test() {
let p = Person()
p.fn = {
p.run()
}
}
test()
- 上面代碼中,
p
對象強引用着fn
閉包,fn
閉包也強引用着p
對象, 自然就造成了循環引用問題 - 最後沒有任何輸出結果, 我們看一下上述代碼的彙編執行過程
- 從上面彙編代碼可以看出, 整個過程經歷了
- 一次
init
引用計數爲----1 - 一次
retain
引用計數會加(1), 結果爲----2 - 一次
release
引用計數會減(1), 結果爲----1 - 那麼最後的引用計數就是1, 所以
p
對象肯定沒有被釋放 - 下面是使用解決循環引用的情況
- 在閉包表達式的捕獲列表裏, 聲明
weak
或unowned
引用,用以解決循環引用問題
- 在閉包表達式的捕獲列表裏, 聲明
// 使用weak
func test() {
let p = Person()
p.fn = { [weak p] in
p?.run()
}
}
// 使用unowned
func test() {
let p = Person()
p.fn = { [unowned p] in
p.run()
}
}
- 上述兩種方式都可以解決循環引用的問題, 運行後就發現
Person
對象調用了deinit
- 這裏我們再看一下彙編代碼如下, 從下面彙編代碼中可以很明顯看到, 引用計數最後爲0, 對象被釋放
另外下面這段代碼其實是等價的
func test() {
let p = Person()
p.fn = { [unowned p] in
p.run()
}
}
// 和上面等價代碼
func test() {
let p = Person()
p.fn = { [unowned ownedP = p, weak weakP = p] in
ownedP.run()
// weakP?.run()
}
}
特別注意點, 這裏要區分捕獲列表和參數列表, 下面看看fn
有參數的情況下
class Person {
var fn: ((Int) -> ())?
func run() { print("run") }
deinit { print("deinit") }
}
func test() {
let p = Person()
p.fn = {
(num) in
print("num = \(num)")
}
}
那麼閉包的參數列表和捕獲列表同事存在的情況如下代碼所示
func test() {
let p = Person()
p.fn = {
[weak p](num) in
print("num = \(num)")
p?.run()
}
}
self的循環引用
- 如果想在引用閉包的同時引用
self
, 這個閉包必須是lazy
的 - 因爲實例在初始化完畢之後才能引用
self
class Person {
lazy var fn: (() -> ()) = {
self.run()
}
func run() { print("run") }
deinit { print("deinit") }
}
func test() {
let p = Person()
p.fn()
}
test()
- 上面代碼中如果
fn
閉包去掉lazy
, 編譯器會直接報錯 - 在
Swift
中, 爲了保證初始化的安全, 設定了兩段式初始化, 在所有的存儲屬性被初始化完成之後, 初始化器才能夠使用self
- 而且在上述
fn
閉包中, 如果fn
內部用到了實例成員(屬性和方法), 則編譯器會強制要求明確寫出self
lazy
既保證只有在使用的時候纔會被初始化一次- 但是上述代碼同樣存在循環引用的問題,
Person
對象強引用着fn
閉包,fn
閉包也強引用着self
- 同樣使用
weak
和unowned
解決循環引用的問題
// weak解決循環引用
lazy var fn: (() -> ()) = {
[weak self] in
self?.run()
}
// unowned解決循環引用
lazy var fn: (() -> ()) = {
[unowned self] in
self.run()
}
另外再看看下面這種情況, 是都存在循環引用的問題
class Student {
var age: Int = 2
lazy var getAge: Int = {
self.age
}()
deinit { print("deinit") }
}
func test() {
let p = Student()
print(p.getAge)
}
test()
/* 輸出結果
2
deinit
*/
通過輸出結果看一看出調用了deinit
, 說明對象最終被釋放, 並未出現循環引用的問題, 下面比較一下
// 存在循環引用
class Person {
lazy var fn: (() -> ()) = {
self.run()
}
func run() { print("run") }
deinit { print("deinit") }
}
// 不存在循環引用
class Student {
var age: Int = 2
lazy var getAge: Int = {
self.age
}()
deinit { print("deinit") }
}
- 上述兩種寫法的區別, 本質上說
Person
對象中的fn
閉包屬於閉包賦值Student
對象那個中的getAge
屬於閉包調用(類似函數調用)- 相當於在在
Student
對象調用getAge
結束之後, 作用域內的變量就會被釋放
// getAge也可以寫成如下形式
lazy var getAge: Int = {
return self.age
}()
// 也可以理解爲
lazy var getAge: Int = self.age
內存訪問衝突
在Swift
中的內存訪問衝突主要在兩個訪問滿足下列條件時發生
- 至少一個是寫入操作
- 它們訪問的是同一塊內存
- 它們的訪問時間重疊(比如在同一個函數內)
- 對比看看以下兩個函數操作
// 不存在內存訪問衝突
var number = 1
func plus(_ num: inout Int) -> Int {
return num + 1
}
number = plus(&number)
// 存在內存訪問衝突
var step = 1
func increment(_ num: inout Int) {
num += step
}
increment(&step)
上面第二部分代碼就是同時對step
變量執行讀寫操作, 運行時會報出如下錯誤
Simultaneous accesses to 0x100002028, but modification requires exclusive access.
再看下面對於結構體和元組的使用, 這裏先定義一個全局函數和一個結構體
// 改變兩個傳入參數的值, 讀取並修改傳入參數的值
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
// 定義Player結構體
struct Player {
var name: String
var health: Int
var energy: Int
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
再看下面的使用示例, 兩者都會有一個內存訪問衝突的錯誤
// 這裏讀寫的是同一個maria
var maria = Player(name: "Maria", health: 50, energy: 10)
balance(&maria.health, &maria.energy)
// 這裏讀寫的是同一個tuple
var tuple = (health: 10, energy: 20)
balance(&tuple.health, &tuple.energy)
但是有時候的確會有上面這種訪問同一塊內存的需求, 如果下面的條件滿足, 就說明重疊訪問結構體的屬性是安全的
- 訪問的是實例存儲屬性, 不是計算屬性或者類屬性
- 結構體是局部變量而非全局變量
- 結構體要麼沒有被閉包捕獲要麼只被非逃逸閉包捕獲
// 這裏可以在局部作用域內定義成局部變量, 就不會有問題了
func test() {
var maria = Player(name: "Maria", health: 50, energy: 10)
var tuple = (health: 10, energy: 20)
balance(&tuple.health, &tuple.energy)
balance(&maria.health, &maria.energy)
}
指針
class Person {}
var person = Person()
- 在
Swift
中class
聲明的類(Person
)是引用類型, 初始化的person
對象其本質上就是一個指針變量 - 而
person
裏面存儲的就是這個指針變量的地址值, 也就可以根據這個地址值去訪問被分配的內存空間 - 指針在某種意義上被定性爲不安全的, 舉個例子:
- 當前指針變量的地址值對應的空間只有32個字節, 但有可能訪問的是超過32個字節的空間, 這樣就可能會出問題的
指針分類
在Swift
中也有專門的指針類型,這些都被定性爲Unsafe
(不安全的),常見的有以下4種類型
UnsafePointer<Pointee>
, 類似於C語言中的const Pointee *
, 只能訪問內存不能修改內存, 這裏的Pointee
是指泛型UnsafeMutablePointer<Pointee>
類似於C語言中的Pointee *
, 可以訪問和修改內存, 這裏的Pointee
是指泛型UnsafeRawPointer
類似於const void *
, 不支持泛型UnsafeMutableRawPointer
類似於void
, 不支持泛型
下面看一下具體的使用示例
var age = 10
func sum1(_ ptr: UnsafeMutablePointer<Int>) {
// 通過訪問pointee屬性, 獲取ptr指針的內存地址所存儲的值
// UnsafeMutablePointer的pointee屬性是可讀可寫的
ptr.pointee += 10
}
func sum2(_ ptr: UnsafePointer<Int>) {
// UnsafePointer的pointee屬性是隻讀的
// ptr.pointee += 10
print(ptr.pointee)
}
func sum3(_ num: inout Int) {
//
num += 10
}
// 和inout輸入輸出參數一樣接受變量的地址值
sum1(&age)
sum2(&age)
sum3(&age)
print(age)
func sum4(_ ptr: UnsafeMutableRawPointer) {
// 可讀可寫, 取值
print("age = ", ptr.load(as: Int.self))
// 可讀可寫, 賦值
ptr.storeBytes(of: 50, as: Int.self)
}
func sum5(_ ptr: UnsafeRawPointer) {
// 只讀, 取值
print("age = ", ptr.load(as: Int.self))
}
sum4(&age)
sum5(&age)
獲得變量的指針
Swift
中有可以直接獲取變量的指針的方法
// 獲取可變的變量指針, value參數接受變量地址
@inlinable public func withUnsafeMutablePointer<T, Result>(to value: inout T, _ body: (UnsafeMutablePointer<T>) throws -> Result) rethrows -> Result
// 獲取不可變的變量指針, value參數接受變量
@inlinable public func withUnsafePointer<T, Result>(to value: T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
// 獲取不可變的變量指針, value參數接受變量地址
@inlinable public func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result
上述方法中返回值默認是變量的指針地址, 也可以是其他的數據類型, 主要取決於body
閉包的返回值, 返回值類型由閉包中的Result
泛型決定
var age = 10
var ptr1 = withUnsafeMutablePointer(to: &age) { $0 } // UnsafeMutablePointer<Int>
var ptr2 = withUnsafePointer(to: &age) { $0 } // UnsafePointer<Int>
ptr1.pointee = 22
print(ptr2.pointee) // 22
print(ptr2) // 0x0000000100008310
var ptr3 = withUnsafeMutablePointer(to: &age) { UnsafeMutableRawPointer($0) } // UnsafeMutableRawPointer
var ptr4 = withUnsafePointer(to: &age) { UnsafeRawPointer($0) } // UnsafeRawPointer
// as參數是需要存儲什麼類型的數據
ptr3.storeBytes(of: 33, as: Int.self)
print(ptr4.load(as: Int.self)) // 33
print(ptr4) // 0x0000000100008310
創建指針
- 之前獲取到的指針都是根據已經存在的內存獲取的
- 這裏就看看重新分配一塊內存指向堆空間
malloc
Swift
提供了malloc
直接分配內存創建指針的方式
// 根據需要分配的內存大小創建一個指針
public func malloc(_ __size: Int) -> UnsafeMutableRawPointer!
// 釋放內存
public func free(_: UnsafeMutableRawPointer!)
// 下面這兩個函數, 是賦值和取值的函數, 之前簡單介紹過
// 參數一: 需要存儲的值
// 參數二: 偏移量, 從第幾個字節開始存儲, 默認從第一個
// 參數三: 需要存儲的值的類型
@inlinable public func storeBytes<T>(of value: T, toByteOffset offset: Int = 0, as: T.Type)
// 參數一: 偏移量, 從第幾個字節開始存儲, 默認從第一個
// 參數二: 需要存儲的值的類型
@inlinable public func load<T>(fromByteOffset offset: Int = 0, as type: T.Type) -> T
代碼示例如下
// 創建指針
var ptr = malloc(16)
// 存儲值
ptr?.storeBytes(of: 10, as: Int.self)
// 這裏toByteOffset參數如果傳0, 就會覆蓋前8個字節的數據
ptr?.storeBytes(of: 12, toByteOffset: 8, as: Int.self)
// 取值
print(ptr?.load(as: Int.self) ?? 0)
print(ptr?.load(fromByteOffset: 8, as: Int.self) ?? 0)
// 銷燬, 釋放內存
free(ptr)
allocate
使用allocate
方式創建指針, 代碼示例如下
// byteCount: 需要申請的字節數, alignment: 對其字節數
var ptr2 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
// 存儲
ptr2.storeBytes(of: 9, as: Int.self)
// 根據字節偏移存儲
// 這裏的ptr3是ptr2偏移8個字節的新的指針地址
var ptr3 = ptr2.advanced(by: 8) // UnsafeMutableRawPointer
ptr3.storeBytes(of: 12, as: Int.self)
// 上面這種方式等價於
ptr2.storeBytes(of: 12, toByteOffset: 8, as: Int.self)
// 取值同樣
print(ptr2.load(as: Int.self))
// 下面這兩種取值方式也是一樣的
print(ptr2.advanced(by: 8).load(as: Int.self))
print(ptr2.load(fromByteOffset: 8, as: Int.self))
// 釋放內存
ptr2.deallocate()
這裏需要注意的是隻有UnsafeMutableRawPointer
纔有allocate
分配方法, UnsafeRawPointer
是沒有這個方法的, 下面說到的UnsafeMutablePointer<T>
類型也是, UnsafePointer<T>
沒有allocate
分配方法
// capacity: 容量, 即可以存儲3個Int類型的數據, 也就是24個字節
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
// 初始化內存, 用10初始化錢8個字節
ptr.initialize(to: 10)
// 用10初始化前兩個容量的內存, 即16個字節
ptr.initialize(repeating: 10, count: 2)
// 使用successor獲取下一個存儲位, 也就是下一個Int的位置
var ptr1 = ptr.successor() // UnsafeMutablePointer<Int>
ptr1.initialize(to: 20)
// 存儲第三個Int值
ptr.successor().successor().initialize(to: 30)
// 取值的兩種方式
print(ptr.pointee) // 第一個值
print((ptr + 1).pointee) // 第二個值
print((ptr + 2).pointee) // 第三個值
// 下面這種方式和上面等價
print(ptr[0])
print(ptr[1])
print(ptr[2])
// 前面如果使用了initialize, 則必須調用反初始化
// 而且count要和上面allocate(capacity: 3)的capacity一致, 否則會造成內存泄露的問題
ptr.deinitialize(count: 3)
ptr.deallocate()
指針之間的轉換
前面提到過Swift
中的指針類型有四種
UnsafePointer<Pointee>
類似於const Pointee *
UnsafeMutablePointer<Pointee>
類似於Pointee *
UnsafeRawPointer
類似於const void *
UnsafeMutableRawPointer
類似於void *
- 那麼上面的類型, 能否通過其中的一種創建另外一種指針呢, 下面我們來看一下
init
UnsafeMutableRawPointer
中有一個初始化方法可以根據UnsafeMutablePointer
創建自身
public init<T>(_ other: UnsafeMutablePointer<T>)
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
var ptr1 = UnsafeMutableRawPointer(ptr)
assumingMemoryBound
反過來, UnsafeMutableRawPointer
也提供了一個方法用於創建UnsafePointer
public func assumingMemoryBound<T>(to: T.Type) -> UnsafePointer<T>
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
var ptr1 = ptr.assumingMemoryBound(to: Int.self)
// 初始化前8個字節
ptr1.pointee = 11
// 初始化後8個字節
// 特別注意, 這裏的(ptr + 8)是指ptr向後偏移8個字節, 要和之前的區分開
(ptr + 8).assumingMemoryBound(to: Int.self).pointee = 12
ptr.deallocate()
unsafeBitCast
unsafeBitCast
是忽略數據類型的強制轉換,不會因爲數據類型的變化而改變原來的內存數
// 把第一個參數類型轉成第二個參數類型
@inlinable public func unsafeBitCast<T, U>(_ x: T, to type: U.Type) -> U
var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
unsafeBitCast(ptr, to: UnsafeMutablePointer<Int>.self).pointee = 13
// 注意, 這裏的(ptr + 8)是指ptr向後偏移8個字節, 要和之前的區分開
unsafeBitCast(ptr + 8, to: UnsafeMutablePointer<Double>.self).pointee = 14.23
ptr.deallocate()
歡迎您掃一掃下面的微信公衆號,訂閱我的博客!