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 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
對象天然就會被銷燬, 毫無疑問上面的輸出結果應該是html
will deinit
Animal deinit
did deinit
複製代碼
一樣下面這段代碼, 一樣也是在a1
對象被置爲nil
的時候內存會被回收, 對象就會被銷燬swift
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 reference
):經過unowned
定義無主引用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
會被調用嗎?bash
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
引用計數爲----1retain
引用計數會加(1), 結果爲----2release
引用計數會減(1), 結果爲----1p
對象確定沒有被釋放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
另外下面這段代碼實際上是等價的微信
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
, 這個閉包必須是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
中的內存訪問衝突主要在兩個訪問知足下列條件時發生ui
// 不存在內存訪問衝突
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
裏面存儲的就是這個指針變量的地址值, 也就能夠根據這個地址值去訪問被分配的內存空間在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
複製代碼
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
方式建立指針, 代碼示例以下
// 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 *
UnsafeMutableRawPointer
中有一個初始化方法能夠根據UnsafeMutablePointer
建立自身
public init<T>(_ other: UnsafeMutablePointer<T>)
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
var ptr1 = UnsafeMutableRawPointer(ptr)
複製代碼
反過來, 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
是忽略數據類型的強制轉換,不會由於數據類型的變化而改變原來的內存數
// 把第一個參數類型轉成第二個參數類型
@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()
複製代碼
歡迎您掃一掃下面的微信公衆號,訂閱個人博客!