- 原文地址:Memory Leaks in Swift: Unit Testing and other tools to avoid them.
- 原文做者:Leandro Pérez
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:RickeyBoy
- 校對者:swants, talisk
本篇文章中,咱們將探討內存泄漏,以及學習如何使用單元測試檢測內存泄漏。如今咱們先來快速看一個例子:html
describe("MyViewController"){
describe("init") {
it("must not leak"){
let vc = LeakTest{
return MyViewController()
}
expect(vc).toNot(leak())
}
}
}
複製代碼
這是 SpecLeaks 中的一個測試。前端
重點:我將要解釋什麼是內存泄漏,討論循環引用以及一些其餘你可能早已知道的事情。若是你僅僅想閱讀有關對泄漏進行單元測試的部分,直接跳到最後一章便可。android
在實際中,內存泄漏是咱們開發者最常面臨的問題。隨着 app 的成長,咱們爲 app 開發了一個又一個的功能,卻也同時帶來了內存泄漏的問題。ios
內存泄漏就是指內存片斷再也不會被使用,卻被永久持有。它是內存垃圾,不只佔據空間也會致使一些問題。git
某個時刻被分配過,但又未被釋放,而且也再也不被你的 app 持有的內存,就是被泄漏的內存。由於它再也不被引用,因此如今沒有辦法釋放掉它,它也沒有辦法被再次使用。github
蘋果官方文檔數據庫
不論咱們是新人仍是老手,咱們總會在某個時間點創造內存泄漏,這無關咱們的經驗多少。爲了打造一個乾淨、不崩潰的應用,消除內存泄漏十分重要,由於它們十分危險。swift
內存泄漏不只會增長 app 的內存佔用,也會引入有害的的反作用甚至崩潰。後端
爲何內存佔用會不斷增加?它是對象沒有被釋放掉的直接後果。這些對象徹底就是內存垃圾,當建立這些對象的操做不斷被執行,它們佔據的內存就會不斷增加。太多的內存垃圾!這可能致使內存警告的狀況,而且最終 app 會崩潰。api
解釋有害的反作用須要更詳細一點的細節。
假設有一個對象在被建立時的 init
方法中開始監聽一個通知。它每次監聽到通知後的動做就是將一些東西存入數據庫中,播放視頻或者是對一個分析引擎發佈一個事件。因爲對象須要被平衡,咱們必需要在它被釋放時中止監聽通知,這在 deinit
中實現。
若是這樣一個對象泄漏了,會發生什麼?
這個對象永遠不會被釋放,它永遠不會中止監聽通知。每一次通知被髮布,該對象就會響應。若是用戶反覆執行操做,建立這個有問題的對象,那麼就會有多個重複對象存在。全部這些對象都會響應這個通知,而且會彼此影響。
在這種狀況下,崩潰多是發生的最好狀況。
大量泄漏的對象重複響應了 app 通知,改變數據庫、用戶界面,使得整個 app 的狀態出錯。你能夠經過 The Pragmatic Programmer 這篇文章中的 Dead Programs tell no lies 瞭解這類問題的重要性。
內存泄漏毫無疑問會致使很是差的用戶體驗以及 App Store 上的低分。
好比第三方 SDK 或者框架均可能產生內存泄漏,甚至也包括 Apple 創造的某些類諸如 CALayer
或者 UILabel
。在這些狀況下,咱們除了等待 SDK 更新或者棄用 SDK 以外別無他法。
但內存泄漏更可能的是由咱們自身的代碼致使的。內存泄漏的頭號緣由則是循環引用。
爲了不內存泄漏,咱們必須理解內存管理和循環引用。
循環這個詞來源於 Objective-C 使用手動引用計數的時期。在可以使用自動引用計數和 Swift,以及咱們如今針對值類型所能作的一切方便的事情以前,咱們使用的是 Objective-C 和手動引用計數。你能夠經過 這篇文章 瞭解手動引用計數和自動引用計數。
在那段時期,咱們須要對內存處理了解更多。理解分配、拷貝、引用的含義,以及如何平衡這些操做(好比釋放)是很是重要的。基本規則是不論你什麼時候創造了一個對象,你就擁有了它而且你須要負責釋放掉它。
如今的事情簡單不少,可是仍然須要學習一些概念。
Swift 中當一個對象對強關聯了另外一個對象,就是引用了它。這裏說的對象指的是引用類型,基本上就是類。
結構體和枚舉都是值類型。僅有值類型的話不太可能產生循環引用。當捕獲和存儲值類型(結構體和枚舉)時,並不會有以前說的關於引用的種種問題。值都是被拷貝的,而不是被引用,儘管值也能持有對對象的引用。
當一個對象引用了第二個對象,那麼就擁有了它。第二個對象將會一直存在直到它被釋放。這被稱做強引用。直到當你將對應屬性設置爲 nil 時第二個對象纔會被銷燬。
class Server {
}
class Client {
var server : Server //Strong association to a Server instance
init (server : Server) {
self.server = server
}
}
複製代碼
強關聯。
A 持有 B 而且 B 持有 A 那麼就形成了循環引用。
A 👉 B + A 👈 B = 🌀
class Server {
var clients : [Client] // 由於這裏是強引用
func add(client:Client){
self.clients.append(client)
}
}
class Client {
var server : Server // 而且這裏也是強引用
init (server : Server) {
self.server = server
self.server.add(client:self) // 這一行產生了循環引用 -> 內存泄漏
}
}
複製代碼
循環引用。
在這個例子中,不論 client 仍是 server 都將沒法被釋放內存。
爲了從內存中釋放,對象必須首先釋放其全部的依賴關係。因爲對象自己也是依賴項,所以沒法釋放。一樣,當一個對象存在循環引用時,它不會被釋放。
當循環引用中的一個引用是**弱引用(weak)或者無主引用(unowned)**的時候,循環引用就能夠被打破。有時候因爲咱們正在編寫的代碼須要相互關聯,所以循環必須存在。但問題就在於不能全部的關聯關係都是強關聯,其中至少必須有一個是弱關聯。
class Server {
var clients : [Client]
func add(client:Client){
self.clients.append(client)
}
}
class Client {
weak var server : Server! // 此處爲弱引用
init (server : Server) {
self.server = server
self.server.add(client:self) // 如今不存在循環引用了
}
}
複製代碼
弱引用能夠打破循環引用。
Swift 提供了兩種方式用以解決使用引用類型時致使的的強引用循環:Weak 和 Unowned。
在循環引用中使用 Weak 以及 Unowned,能讓一個實例引用另外一個實例時再也不保持強持有。這樣實例之間可以互相引用而不會產生強引用循環。
Weak: 一個變量可以可選地不持有其引用的對象。當變量並不持有其引用對象時,就是弱引用。弱引用能夠爲 nil。
Unowned: 和弱引用類似,無主引用也不會強持有其引用的實例。但與弱引用不一樣的是,無主引用必須是一直有值的。正因如此,無主引用始終被定義爲非可選類型。無主引用不能爲 nil。
當閉包和它捕獲的實例互相引用時,將閉包中的捕獲值定義爲無主引用,這樣他們老是會同時被釋放出內存。
相反的,將閉包中捕獲的實例定義爲弱引用時,這個捕獲的引用有可能在將來變成
nil
。弱引用始終是一個可選類型,當引用的實例被釋放出內存時它就會自動變成nil
。
class Parent {
var child : Child
var friend : Friend
init (friend: Friend) {
self.child = Child()
self.friend = friend
}
func doSomething() {
self.child.doSomething( onComplete: { [unowned self] in
//The child dies with the parent, so, when the child calls onComplete, the Parent will be alive
self.mustBeAlive()
})
self.friend.doSomething( onComplete: { [weak self] in
// The friend might outlive the Parent. The Parent might die and later the friend calls onComplete.
self?.mightNotBeAlive()
})
}
}
複製代碼
對比弱引用和無主引用。
寫代碼時忘記使用 weak self
的狀況並不稀奇。咱們常常在寫閉包時引入內存泄漏,好比在使用 flatMap
和 map
這樣的函數式代碼時,或者是在寫消息監聽、代理的相關代碼時。這篇文章 裏你能夠讀到更多關於閉包中內存泄漏的內容。
weak self
也將容易被發現。代碼審查也能提供很大幫助。一旦咱們知道循環和弱引用是怎麼一回事,咱們就能爲循環引用編寫測試,方法就是弱引用去檢測循環。只須要對某個對象進行弱引用,咱們就能測試出該對象是否有內存泄漏。
由於弱引用並不會持有其引用的實例,因此當實例被釋放出內存時,極可能弱引用仍然指向該實例。所以,當弱引用引用的對象被釋放後,自動引用計數會將弱引用設置爲
nil
。
假設咱們想知道 x
是否發生了內存泄漏,咱們建立了一個指向它的弱引用,叫作 leakReference
。若是 x
被從內存中釋放,ARC 會將 leakReference
設置爲 nil。因此,若是 x
發生了內存泄漏,leakReference
永遠不會被設置爲 nil。
func isLeaking() -> Bool {
var x : SomeObject? = SomeObject()
weak var leakReference = x
x = nil
if leakReference == nil {
return false // 沒發生內存泄漏
}
else{
return true // 發生了內存泄漏
}
}
複製代碼
測試一個對象是否發生內存泄漏。
若是 x
真的發生了內存泄漏,弱引用 leakReference
會指向這個發生內存泄漏的實例。另外一方面,若是該對象沒發生內存泄露,那麼在該對象被設置爲 nil 以後,它將再也不存在。這樣的話,leakReference
將會爲 nil。
」Swift by Sundell」 在 這篇文章 中詳細闡述了不一樣內存泄漏的區別,對我寫本文以及 SpecLeaks 都有極大的幫助。另外 一篇佳做 也採用了相似的方式。
基於這些理論,我寫出了 SpecLeacks,一個基於 Quick 和 Nimble、可以檢測內存泄漏的拓展。核心就是編寫單元測試來檢測內存泄漏,不須要大量冗餘的樣板代碼。
結合使用 Quick 和 Nimble 能更好地編寫更人性化、可讀性更強的單元測試。SpecLeaks 只是在這兩個框架的基礎之上增長了一點點功能,使其可以讓你更方便地編寫單元測試,來檢測是否有對象發生了內存泄漏。
若是你對單元測試並不瞭解,那麼這張截圖也許可以給你一個提示,告訴你單元測試作了些什麼:
你能夠寫單元測試來實例化一些對象,並在基於它們作一些嘗試。你定義指望的結果,以及怎樣的結果纔算符合預期,才能經過測試,讓測試結果呈現綠色。若是最終結果並不符合最開始定義的預期,那麼測試將會失敗並呈現出紅色。
這是檢測內存泄漏的測試中,最簡單的一個,只須要初始化一個實例並看它是否發生了內存泄漏。有時,這個對象註冊了監聽事件,或者是有代理方法,或者註冊了通知,這些狀況下,這類測試就能檢測出一些內存泄漏:
describe("UIViewController"){
let test = LeakTest{
return UIViewController()
}
describe("init") {
it("must not leak"){
expect(test).toNot(leak())
}
}
}
複製代碼
測試初始化階段。
一個 viewController 可能在它的子視圖加載完成後開始發生內存泄漏。在此以後,會發生大量的事情,可是使用這個簡單的測試你就能保證在 viewDidLoad 方法中不存在內存泄漏。
describe("a CustomViewController") {
let test = LeakTest{
let storyboard = UIStoryboard.init(name: "CustomViewController", bundle: Bundle(for: CustomViewController.self))
return storyboard.instantiateInitialViewController() as! CustomViewController
}
describe("init + viewDidLoad()") {
it("must not leak"){
expect(test).toNot(leak())
//SpecLeaks will detect that a view controller is being tested
// It will create it's view so viewDidLoad() is called too } } } 複製代碼
對一個 viewController 的 init 和 viewDidLoad 進行測試。
使用 SpecLeaks 你不須要爲了使 viewDidLoad
方法被調用而手動調用 viewController 上的 view
。當你測試 UIViewController
的子類時 SpecLeaks 將會替你作這些。
有時候初始化一個實例並不能判斷是否發生了內存泄漏,由於內存泄漏有可能在某個方法被調用的時候發生。在這種狀況下,你能夠在操做被執行的時候測試是否有內存泄漏,像這樣:
describe("doSomething") {
it("must not leak"){
let doSomething : (CustomViewController) -> () = { vc in
vc.doSomething()
}
expect(test).toNot(leakWhen(doSomething))
}
}
複製代碼
檢測自定義 viewController 是否在 doSomething
方法被調用時發生內存泄漏。
內存泄漏能產生大量問題,他們會致使極差的用戶體驗、崩潰和 App Store 中的差評,咱們必需要消除它們。良好的代碼風格、良好的實踐、對內存管理透徹的理解以及單元測試都能起到有效的幫助。
可是單元測試並不能保證內存測試徹底不發生,你並不能覆蓋全部的方法調用和狀態,測試每個存在與其餘對象相互做用的東西是不太可能的。另外,有時候必需要模擬依賴,才能發現原始的依賴可能發生的內存泄漏。
單元測試確實能下降發生內存泄漏的可能性,使用 SpeakLeaks 能夠很是方便的檢測、發現出閉包中的內存泄漏,就好比 flatMap
或者是其餘持有了 self
的逃逸閉包。若是你忘記將代理聲明爲弱引用也是一樣的道理。
我大量地使用了 RxSwift,以及 faltMap、map、subscribe 和一些其餘須要傳遞閉包的函數。在這些狀況下,缺乏 weak 或 unowned 常常會致使內存泄漏,而使用 SpecLeaks 就能輕易的檢測出來。
就我的而言,我始終嘗試在個人全部類之中增長這樣的測試。例如每當我創造一個 viewController,我就會爲它創造一份 SpecLeaks 代碼。有時候 viewController 會在加載視圖時發生內存泄漏,用這類測試就能垂手可得地發現。
那麼你意下如何?你會爲檢測內存泄漏而寫單元測試嗎?你會寫測試嗎?
我但願你喜歡閱讀本文,若是你有任何的建議和疑問均可以給我回復!請盡情嘗試 SpeckLeaks :)
感謝 Flawless App。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。