Swift使用自動引用計數(ARC)來管理應用程序的內存使用。這表示內存管理已經是Swift的一部分,在大多數情況下,你並不需要考慮內存的管理。當實例並不再被需要時,ARC會自動釋放這些實例所使用的內存。
另外需要注意的:
引用計數僅僅作用於類實例上。結構和枚舉是值類型,而非引用類型,所以不能被引用存儲和傳遞。
swift的ARC工作過程
每當創建一個類的實例,ARC分配一個內存塊來存儲這個實例的信息,包含了類型信息和實例的屬性值信息。
另外當實例不再被使用時,ARC會釋放實例所佔用的內存,這些內存可以再次被使用。
但是,如果ARC釋放了正在被使用的實例,就不能再訪問實例屬性,或者調用實例的方法了。直接訪問這個實例可能造成應用程序的崩潰。就像空實例或遊離實例一樣。
爲了保證需要實例時實例是存在的,ARC對每個類實例,都追蹤有多少屬性、常量、變量指向這些實例。當有活動引用指向它時,ARC是不會釋放這個實例的。
爲實現這點,當你將類實例賦值給屬性、常量或變量時,指向實例的一個強引用(strong
reference)將會被構造出來。被稱爲強引用是因爲它穩定地持有這個實例,當這個強引用存在時,實例就不能夠被自動釋放,因此可以安全地使用。
<pre name="code" class="objc">class Teacher
{
var tName : String
init(name:String)
{
tName = name
println("老師 \(tName) 實例初始化完成.")
}
func getName() -> String
{
return tName
}
func classing()
{
println("老師 \(tName) 正在給學生講課.")
}
deinit
{
println("老師 \(tName) 實例析構完成.")
}
}
測試 ARC
func testArc()
{
var teacher:Teacher? = Teacher(name:"張三") //實例化一個Teacher對象將指向一個變量,此時產生了一個強引用(就好像OC中的引用計數+1)
var refteacher:Teacher? = teacher //再次產生強引用即(引用計數再+1)
var refteacher2:Teacher? = teacher<span style="white-space:pre"> </span> <span style="font-family: Arial, Helvetica, sans-serif;">//再次產生強引用即(引用計數再+1)</span>
refteacher = nil //第一個引用對象爲nil並沒有使實例釋放,(引用計數-1)
teacher?.classing() //正常
teacher = nil //第二個引用對象爲nil並沒有使實例釋放,(引用計數-1)
refteacher2!.classing() //正常
refteacher2 = nil //第三個引用對象爲nil此時已沒有作何引用了,因此ARC回收,實例釋放.(引用計數-1)最後引用計數爲0,則自動調用析構
refteacher2?.classing() //不再有輸出
}
輸出結果
老師 張三 實例初始化完成.
老師 張三 正在給學生講課.
老師 張三 正在給學生講課.
老師 張三 實例析構完成.
從上面的例子來看,確實swift給我們自動管理了內存,很多時侯開發者都不需要考慮太多的內存管理。但真的是這樣嗎?真的安全嗎?作爲開發者要如何用好ARC?
儘管ARC減少了很多內存管理工作,但ARC並不是絕對安全的。下面來看一下循環強引用導至的內存泄漏
class Teacher
{
var tName : String
var student : Student? //添加學生對象,初始時爲nil
init(name:String)
{
tName = name
println("老師 \(tName) 實例初始化完成.")
}
func getName() -> String
{
return tName
}
func classing()
{
println("老師 \(tName) 正在給學生 \(student?.getName()) 講課.")
}
deinit
{
println("老師 \(tName) 實例析構完成.")
}
}
class Student
{
var tName : String
var teacher : Teacher? //添加老師對象,初始時爲nil
init(name:String)
{
tName = name
println("學生 \(tName) 實例初始化完成.")
}
func getName() -> String
{
return tName
}
func listening()
{
println("學生 \(tName) 正在聽 \(teacher?.getName()) 老師講的課")
}
deinit
{
println("學生 \(tName) 實例析構化完成.")
}
}
測試泄漏:
func testMemoryLeak()
{
var teacher :Teacher?
var student :Student?
teacher = Teacher(name:"陳峯") //(引用計數爲1)
student = Student(name:"徐鴿") //<span style="font-family: Arial, Helvetica, sans-serif;">(引用計數爲1)</span>
teacher!.student = student //賦值後將產生"學生"對象的強引用 (引用計數+1)
student!.teacher = teacher //賦值後將產生"老師"對象的強引用 (引用計數+1)
teacher!.classing() //因爲我清楚地知道teacher對象不可能爲空,所以我用!解包
student!.listening()
//下面的代碼,寫與不寫都不能使對象釋放
teacher = nil //引用計數-1 但還不能=0,所以不會析構
student = nil<span style="white-space:pre"> </span> //引用計數-1 但還不能=0,所以也不會析構
println("釋放後輸出")
teacher?.classing()<span style="white-space:pre"> </span>//因爲我不能確定teacher對象是否爲空,所以必須用?來訪問。
student?.listening()
}
輸出結果:
老師 陳峯 實例初始化完成.
學生 徐鴿 實例初始化完成.
老師 陳峯 正在給學生 徐鴿 講課.
學生 徐鴿 正在聽 陳峯 老師講的課
釋放後輸出
自始至終都沒有調用deinit。因此就會泄漏,此時已經不能採取任何措拖來釋放這兩個對象了,只有等APP的生命週期結束
實例之間的相互引用,在日常開發中是很常見的一種,哪麼如何避免這種循環強引用導致的內存泄漏呢?
可以通過在類之間定義爲弱引用(weak)或無宿主引用的(unowned)變量可以解決強引用循環這個問題
弱引用方式:
弱引用並不保持對所指對象的強烈持有,因此並不阻止ARC對引用實例的回收。這個特性保證了引用不成爲強引用循環的一部分。指明引用爲弱引用是在生命屬性或變量時在其前面加上關鍵字weak。
注意
弱引用必須聲明爲變量,指明它們的值在運行期可以改變。弱引用不能被聲明爲常量。
因爲弱引用可以不含有值,所以必須聲明弱引用爲可選類型。因爲可選類型使得Swift中的不含有值成爲可能。
因此只需要將上述的例子任意一個實例變量前加上weak關鍵詞即可,如:
weak var student : Student?
或
weak var teacher : Teacher?
下面來測試一下weak var student : Student?設爲弱引用後,測試釋放的時間點(情況一)
var teacher :Teacher?
var student :Student?
teacher = Teacher(name:"陳峯")
student = Student(name:"徐鴿")
teacher!.student = student //賦值後將產生"學生"對象的強引用
student!.teacher = teacher //賦值後將產生"老師"對象的強引用
teacher!.classing()
student!.listening()
teacher = nil //此時將沒有馬上調用析構,要等student釋放後纔會釋放
//student = nil
println("釋放後輸出")
teacher?.classing()<span style="white-space:pre"> </span>//前面已設爲nil,所以沒有輸出
student?.listening()
經測試輸出
老師 陳峯 實例初始化完成. //執行teacher = Teacher(name:"陳峯")
學生 徐鴿 實例初始化完成. //執行student = Student(name:"徐鴿")
老師 陳峯 正在給學生 徐鴿 講課. //執行teacher!.classing()
學生 徐鴿 正在聽 陳峯 老師講的課 //執行student!.listening()
釋放後輸出 //執行println("釋放後輸出")
學生 徐鴿 正在聽 陳峯 老師講的課 //執行student?.listening()
學生 徐鴿 實例析構化完成. //學生對象先釋放
老師 陳峯 實例析構完成. //此時由於學生對象釋放了,此時沒有了引用,也可以進行析構了
如果 weak var teacher : Teacher?
再來進行測試:(情況二)
var teacher :Teacher?
var student :Student?
teacher = Teacher(name:"陳峯")
student = Student(name:"徐鴿")
teacher!.student = student //賦值後將產生"學生"對象的強引用
student!.teacher = teacher //賦值後將產生"老師"對象的強引用
teacher!.classing()
student!.listening()
teacher = nil //此時將沒有馬上調用析構,要等student釋放後纔會釋放
//student = nil
println("釋放後輸出")
teacher?.classing()
student?.listening() //此時並不因爲
輸出結果:
老師 陳峯 實例初始化完成.
學生 徐鴿 實例初始化完成.
老師 陳峯 正在給學生 徐鴿 講課.
學生 徐鴿 正在聽 陳峯 老師講的課
老師 陳峯 實例析構完成.
釋放後輸出
學生 徐鴿 正在聽 nil 老師講的課
學生 徐鴿 實例析構化完成.
經測試得出結論:
當A類中包函有B類的弱引用的實例,同時,B類中存在A的強引用實例時,如果A釋放,也不會影響B的析放,但A的內存回收要等B的實例釋放後纔可以回收。(情況一的結果)
當A類中包函有B類的強引用的實例時,如果A釋放,則不會影響B的析放。(情況二的結果)
和弱引用一樣,無宿主引用也並不持有實例的強引用。但和弱引用不同的是,無宿主引用通常都有一個值。因此,無宿主引用並不定義成可選類型。指明爲無宿主引用是在屬性或變量聲明的時候在之前加上關鍵字unowned。
因爲無宿主引用爲非可選類型,所以每當使用無宿主引用時不必使用?。無宿主引用通常可以直接訪問。但是當無宿主引用所指實例被釋放時,ARC並不能將引用值設置爲nil,因爲非可選類型不能設置爲nil。
注意
在無宿主引用指向實例被釋放後,如果你想訪問這個無宿主引用,將會觸發一個運行期錯誤(僅當能夠確認一個引用一直指向一個實例時才使用無宿主引用)。在Swift中這種情況也會造成應用程序的崩潰,會有一些不可預知的行爲發生。因此使用時需要特別小心。
將前面例子改爲無宿主引用:
class Teacher
{
var tName : String
var student : Student? //學生對象的強引用,實例可以爲nil
init(name:String)
{
tName = name
println("老師 \(tName) 實例初始化完成.")
}
func getName() -> String
{
return tName
}
func classing()
{
println("老師 \(tName) 正在給學生 \(student?.getName()) 講課.")
}
deinit
{
println("老師 \(tName) 實例析構完成.")
}
}
class Student
{
var tName : String
unowned var teacher : Teacher //無宿主引用,不可以設置爲nil
init(name:String,tcher :Teacher)
{
tName = name
teacher = tcher //因爲無宿主引用不能設爲可選型,所在必須要初始化
println("學生 \(tName) 實例初始化完成.")
}
func getName() -> String
{
return tName
}
func listening()
{
println("學生 \(tName) 正在聽 \(teacher.getName()) 老師講的課")
}
deinit
{
println("學生 \(tName) 實例析構化完成.")
}
}
測試無宿主引用:
func testNotOwner()
{
var teacher :Teacher? //聲明可選型變量
teacher = Teacher(name:"陳峯")
var student = Student(name: "徐鴿",tcher: teacher!)
//進行相互引用
teacher!.student = student
student.teacher = teacher!
teacher!.classing()
student.listening()
teacher = nil
println("老師對象釋放後")
teacher?.classing()
student.listening() //error 因爲在前面的teacher設爲nil時,隱式的將student對象給釋放了,因此這裏再訪問就會crash
}
輸出結果:
老師 陳峯 實例初始化完成.
學生 徐鴿 實例初始化完成.
老師 陳峯 正在給學生 徐鴿 講課.
學生 徐鴿 正在聽 陳峯 老師講的課
老師 陳峯 實例析構完成.
老師對象釋放後
Program ended with exit code: 9(lldb) //會crash,thead1:Exc_BREAKPOINT(code=EXC_i386_BPT,subcode=0x0)
所以使用無宿主引用時,就需要特別小心,小心別人釋放時,順帶釋放了強引用對象,所以要想別人釋放時不影響到原實例,可以使用弱引用這樣就算nil,也不會影響。
上面介紹了,當某個類中的實例對象如果在整個生命週期中,有某個時間可能會被設爲nil的實例,使用弱引用,如果整個生命週期中某一實例,一旦構造,過程中不可能再設爲nil的實例變量,通常使用無宿主引用。但時有些時侯,在兩個類中的相互引用屬性都一直有值,並且都不可以被設置爲nil。這種情況下,通常設置一個類的實例爲無宿主屬性,而另一個類中的實例變量設爲的隱式裝箱可選屬性(即!號屬性)
如下面的例子,每位父親都有孩子(沒孩子能叫父親麼?),每個孩子都有一個親生父親
class Father
{
let children : Children! //聲明爲隱式可選類型
let fathername : String
init(name:String,childName:String)
{
self.fathername = name
self.children = Children(name: childName,fat:self) //初始化時產生相互引用
}
deinit
{
println("father deinited.")
}
}
class Children
{
unowned let father : Father //聲明爲無宿主類型
let name : String
init(name:String ,fat : Father)
{
self.name = name
self.father = fat
}
deinit
{
println("children deinited.")
}
}
測試代碼:
var fa = Father(name: "王五",childName: "王八")
println("\(fa.fathername) 有個小孩叫 \(fa.children.name)")
輸出結果:
王五 有個小孩叫 王八
father deinited.
children deinited.
同樣可以看到,儘管是循環引用,但還是能正常回收。
另外,還有一種情況,當自身的閉包對自身(self) 的強引用,也會導致內存泄漏。
例子:
class CpuFactory
{
let cpuName : String
let cpuRate : Double
init(cpuName:String,rate:Double)
{
self.cpuName = cpuName
self.cpuRate = rate
}
//聲明一個閉包
@lazy var someClosure: (Int, String) -> String = {
//下面這句不可以註釋編譯器會報Tuple types '(Int,String)'and'()'hava a different number of elements (2 vs. 0)
[unowned self] (index: Int, stringToProcess: String) -> String in
// closure body goes here
return "A \(self.cpuName)" //閉包中引用self
}
//聲明一個閉包,同樣閉包中引用self
@lazy var machining: () -> String = {
[unowned self] in //這句可以註釋(按照書上說,使用這句可以解釋閉包的強引用,但個人實踐,不管加不加這句,都不會釋放,即這樣寫有內存泄漏)
// closure body goes here
if self.cpuRate > 10
{
return "\(self.cpuName) i7 2.5G"
}
else
{
return "\(self.cpuName) i3 2.0G"
}
}
//聲明一個閉包,但閉包中將自身作爲參數傳進去(可以避去內存泄漏)
@lazy var machining2 : (CpuFactory) -> String = {
[unowned self] (cpu:CpuFactory) -> String in
if cpu.cpuRate > 10
{
return "\(cpu.cpuName) i7 2.5G"
}
else
{
return "\(cpu.cpuName) i3 2.0G"
}
}
deinit
{
println("Cpu Factroy is deinited.")
}
}
在這個例子中有三個閉包,分別是帶參,和不帶參,對於帶參的 不能省略[unowned self] (paramers) in操作。否則會編譯不過,另外,書中沒有提到的,只有聲明爲@lazy的閉包中纔可以使用[unowned self] 否則在普通閉包中使用也會報錯。還有一點書中講到當自身閉包中使用self.時會產生強引用,導至內存泄漏,因此加上[unowned self ] in 這句可以破壞這種強引用,從而使內存得到釋放,但經本人親自驗證,就算加上了也沒有釋放。
測試
func testClosure()
{
var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
// println(cpu!.machining())
println(cpu!.machining2(cpu!))
// println(cpu!.someClosure(3,"hello"))
cpu = nil
}
分別單獨驗證各句輸出結果:
func testClosure()
{
var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
println(cpu!.machining())
cpu = nil
}
輸出:
Core i3 2.0G
顯然cpu = nil也不會釋放內存。
再來看第二個。
func testClosure()
{
var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
println(cpu!.machining2(cpu!))
cpu = nil
}
輸出
Core i3 2.0G
Cpu Factroy is deinited.
可見使用自身作爲參數傳參時,可以釋放內存。
同樣再測試第三種
func testClosure()
{
var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
println(cpu!.someClosure(3,"hello"))
cpu = nil
}
輸出
A Core
其實第三和第一種是一樣的,都是引用了self.但第一種可以把[unowned self ]in 句註釋和不註釋的情況下進行測試,可以發現結果是一樣的,並沒有釋放內存。
實在令人有點費解。。。。。。