啟航之Swift語言下的Instruments使用教程

圖1

學(xué)習(xí)如何使用【Xcode Instruments】來進行錯誤排查和優(yōu)化代碼。

更新提示:這篇教程由James Frost進行更新以適配iOS 8 和 Swift語言,原始教程由教程組成員Matt Galloway提供。

不管你是開發(fā)過眾多iOS App的老手,還是正在著手于你的第一個App的新手:但是毫無疑問的是,你要使用最新的語言特性,并且想知道怎么做才能讓你的App變得更好。

要想改進完善你的App,除了盡可能使用最新的語言特性外,還有一件所有優(yōu)秀的app開發(fā)者應(yīng)該做的——instrument你的代碼!

PS:(instrument:儀器、工具;用儀器,用工具。文中直接使用英文單詞,各人根據(jù)自己喜好定含義。)

這篇教程將向你展示如何使用Xcode自帶的調(diào)試工具Instruments的幾個最重要的方面。該工具可以幫你檢查代碼中涉及到的性能問題、內(nèi)存問題、循環(huán)引用問題以及其他許多難題。

在這篇教程中你將學(xué)到以下內(nèi)容:

1、如何使用Instruments中的Time Profiler選項來找出代碼中最耗時的“熱點”,以改進,使代碼的運行效率更高。

2、如何使用Instruments中的Allocations選項,檢測并修復(fù)代碼中的內(nèi)存管理問題,比如強循環(huán)引用。

注意:在這篇課程中,我們假設(shè)你對Swift語言和iOS編程已經(jīng)相當(dāng)?shù)氖煜?。但是如果你是個iOS編程方面的新手,你或許還需要瀏覽一下該網(wǎng)站上的其他內(nèi)容。本課程使用到了storyboard(故事板),所以請確保你已經(jīng)熟悉storyboard這個東西。如果不熟悉,請瀏覽這個鏈接

準(zhǔn)備好了嗎?我們即將開始深入探索迷人的Instruments世界了。

正式開始了:

在此,你不需要為此課程從頭開始創(chuàng)建一個全新的應(yīng)用程序,因為,我們已經(jīng)給你提供了一個樣例工程。你所需要的做的就是通覽整個程序,根據(jù)instruments給出的指引改進這個程序,就像你要優(yōu)化自己的app一樣。

點擊下載樣例工程,然后解壓并使用Xcode打開。

這個樣例程序使用了Flickr API來搜索圖片。要想使用Flickr API你需要一個API key。在演示工程中,你可以通過Flickr網(wǎng)站來生成一個樣例key。具體做法是,只要通過下面的網(wǎng)址,執(zhí)行一個搜索即可:

http://www.flickr.com/services/api/explore/?method=flickr.photos.search

在返回的結(jié)果網(wǎng)址中,拷貝出URL中的API key即可,該key的內(nèi)容是“&api_key=” 和下一個“&”中間的部分。

比如,如果我們得到的URL是下面這樣的:

http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=6593783

efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f

那么我們所需要的API key就是:6593783efea8e7f6dfc6b70bc03d2afb。

然后將上面那個key粘貼到FlickrSearcher.swift?文件的頂部替換掉原來的API key即可。

注意這個樣例API key可能每天都在變化,所以你可能偶爾需要重新生成一個新的key。不論什么時候,如果這個key不再有效,app都會給出警告的。

接著,編譯并運行這個app,然后執(zhí)行一個搜索,搜索結(jié)果出來后,點擊搜索結(jié)果,你將會看到類似下面圖片中的內(nèi)容:

圖2

通覽整個應(yīng)用程序并檢查其中的基礎(chǔ)函數(shù)。你可能忍不住會想一旦App的UI界面運行良好,就可以將代碼提交到庫中了。然而,請等一等,接下來你將看到使用instruments能給的App帶來的好處。

余下的課程將向你展示,如何找到并修復(fù)App中仍然存在的問題。你將會看到使用Instruments調(diào)試程序中的問題是多么的簡單。

時間性能檢測

圖3

你第一個接觸到的Instruments選項是Time Profiler。在每個測量的時間間隔內(nèi),Instruments將會暫停程序的執(zhí)行,并記錄每個正在運行著的線程的堆棧執(zhí)行情況(堆棧軌跡)。想象一下,這就像你在Xcode的調(diào)試模式下運行程序時按下了暫停按鈕。

這里是一張Time Profiler運行時的預(yù)覽圖:

圖4

這張屏幕截圖中展示了Call Tree(調(diào)用層次樹)。Call Tree能夠顯示出app中不同方法執(zhí)行時所花費的時間。圖中的每一行代表著在程序執(zhí)行過程中所追蹤到的不同方法。每個方法所花費的時間可以通過Profile在其上停留的次數(shù)來斷定。

比如,每1毫秒完成一次采樣,現(xiàn)在完成了100次采樣,此時我們在棧頂端發(fā)現(xiàn)某一方法被采樣10次,那么你可以估測大約10%的運行時間——10毫秒——花在了該方法上。雖然這只是比較粗略的估計,但卻是有效的。

注意:一般情況下,你總應(yīng)該在真機上檢測你的程序,而不是在模擬器上。iOS模擬器使用的是電腦的硬件資源(遠超移動設(shè)備的硬件性能),而真機會受限于其移動設(shè)備的硬件性能。有時候你會發(fā)現(xiàn)你的app在模擬器上運行情況良好,但是在真機上運行時,會出現(xiàn)一些性能問題。

好了,還是不要杞人憂天了,是時候開始檢測了。

從Xcode的菜單欄中,依次選中菜單項:Product\Profile,或者按組合鍵? + I。接著將編譯程序,并運行Instruments。你將會看到類似下面的一個選項窗口界面:

圖5

這里面列出了Instruments自帶的所有的工具模版選項。

選中Time Profiler項并點擊右下角的Choose按鈕,這將打開一個新的Instruments運行界面。在新出現(xiàn)的界面中,點擊左上角的紅色按鈕啟動程序并開始記錄。此時,你可能會被要求輸入密碼以授權(quán)Instruments分析其他進程——不過不用擔(dān)心,在此提供密碼是安全的。

在Instruments窗口中,你可以看到時間在累加,同時在屏幕中央的圖表上方,一個小箭頭在從左到右的移動著。這意味著app正在運行。

現(xiàn)在,操作你的app。

搜索幾張圖片,并層層深入點擊一個或多個搜索結(jié)果,你或許已經(jīng)注意到了,要查看一個搜索結(jié)果非常的慢,并且要對所有的搜索結(jié)果進行滾動操作也非常的慢——這是一個體驗非常差的app。

不過,你還是很幸運的,因為你即將著手修復(fù)上述問題。然而,你只是第一次淺顯的接觸Instruments。所以,首先,確保工具欄上右上角的視圖選擇按鈕都被選中。像下圖中的這樣:

圖6

這將確保所有的輸出面板都被打開?,F(xiàn)在,我們開始學(xué)習(xí)下面屏幕截圖中的內(nèi)容以及圖片下方對每一項的解釋:

圖7

1、這里是錄制控制部分,當(dāng)你點擊紅色的“錄制”按鈕時,將開啟或關(guān)閉當(dāng)前正在監(jiān)測的app(當(dāng)該按鈕被觸發(fā)時,其狀態(tài)在“錄制”和“停止”之間切換)?!皶和!卑粹o暫停當(dāng)前app的執(zhí)行。

2、這是執(zhí)行計時器,該計時器計算當(dāng)前被檢測的app運行了多長時間,以及被執(zhí)行了幾次。如果你使用錄制控制器停止并重啟當(dāng)前的app,那么將會重新運行一次Time Profiler并且該計時器上將顯示:Run 2 of 2

3、這里是顯示的是某個工具的使用軌道。當(dāng)你只選擇Time Profiler模版這一項時,只顯示一個軌道。在后面的課程中你將學(xué)習(xí)到更多在這里顯示的圖表的相關(guān)細節(jié)。

4、這里顯示的是詳情面板。它呈現(xiàn)的是你當(dāng)前正在使用的Instruments配置選項的主要信息。在目前這種選項下,它能夠顯示出使用CPU時間最多的所謂“最火”的方法。

5、這是檢查器面板。這里包含三種檢查器:錄制設(shè)置、顯示設(shè)置、和擴展信息。很快你就可以學(xué)到有關(guān)這些選項的更多內(nèi)容。

現(xiàn)在,我們開始修復(fù)那個體驗極差的UI。

深入了解

執(zhí)行一次圖片搜索,并深入搜索結(jié)果。我個人喜歡搜索“狗”,當(dāng)然你也可以搜索其他東西,比如:“貓”:

現(xiàn)在,對搜索結(jié)果列表做幾次上下滾動的操作,這樣就可以為Time Profiler獲取到大量的分析數(shù)據(jù)。你應(yīng)該注意到了屏幕中部的數(shù)字改變了,上述的軌道圖表也被不同的顏色填滿了;這是在向你傳達該程序使用CPU的情況。

你肯定不喜歡像現(xiàn)在這樣糟糕的UI——列表竟然沒能及時加載數(shù)據(jù)!要精確地定位該問題,你需要對某些選項做一些配置。

在右手邊,選擇Display Setting檢查器(或者快捷鍵?+2)。在檢查器窗口中,在Call Tree選項下面, 選擇Separate by Thread、Invert Call TreeHide Missing SymbolsHide System Libraries。操作完成后,界面看起來應(yīng)該是這樣的:

圖8

這里是各個選項如何對左邊列表中的數(shù)據(jù)的顯示產(chǎn)生影響的。

1、Separate by Thread: 每個線程都應(yīng)該單獨對待。這樣可以讓你知道到底哪個線程占用了最多的CPU周期。

2、Invert Call Tree: 利用這個選項,堆棧使用情況按照從上到下的方式排列。通常情況下,這也是你想要的,因為你可能想看到最深一層的方法調(diào)用以及其所占CPU時間周期。

3、Hide Missing Symbols: 如果你的app或者system framework(系統(tǒng)框架文件)的dSYM文件沒有被找到,那么你將看到的是方法在庫中的十六進制地址,而不是方法名(symbols)。如果該選項被勾選,那么你將看到的是完整的方法名,而不是難以理解的十六進制數(shù)字。這可以幫你優(yōu)化當(dāng)前數(shù)據(jù)的顯示。

4、Hide System Libraries: 當(dāng)這個選項被勾選時,只有你自己app中的方法名會被顯示。通常情況下,勾選這個選項還是很有用的,因為你只會關(guān)注自己代碼所使用的CPU時間——當(dāng)然你不會關(guān)注,也無法控制系統(tǒng)代碼對CPU的使用。

5、Flatten Recursion: 這個選項在每個堆棧上把遞歸函數(shù)(自己調(diào)用自己的函數(shù))作為一個條目來對待,而不是多條。

6、Top Functions: 啟用這個選項可以讓Instruments這樣計算一個函數(shù)花費的總時間——自己本身花費的時間和內(nèi)部調(diào)用其他函數(shù)花費的時間之和。舉個例子,函數(shù)A調(diào)用函數(shù)B,那么我們看到的花費在A上的時間就是A本身花費的時間加上花費在B上的時間的總和。這樣做非常有用,這可以讓你每次從調(diào)用堆棧中按照降序的方式獲取到最大的時間數(shù)字,讓你集中精力關(guān)注那個花費最多時間的方法。

7、如果你正在運行的是Objective-C app,這里還會出現(xiàn)一個選項Show Obj-C Only:此時,如果這個選項被選中,那么只有Objective-C類型的函數(shù)會被顯示,C、C++類型的函數(shù)則不會被顯示。當(dāng)你的程序中沒有C、C++類型的函數(shù)時,該選項沒有什么作用,但是如果我們正在運行的是一個OpenGL app,其中很可能會有一些C++函數(shù),此時該選項就可以發(fā)揮做用了。

雖然在一些數(shù)值上可能會有細微的差別,但是一旦你啟用上述的那些選項,相關(guān)條目的排列順序應(yīng)該與下表相似:

圖9

嗯,那看起來確實很糟糕。大部分的時間都花在了方法applyTonalFilter上面了,不過這不應(yīng)該讓你感到很震驚,因為表格的加載和滾動才是UI體驗最差的部分,尤其是在表格單元不斷更新的時候。

要找出有關(guān)該方法更多的內(nèi)部細節(jié),雙擊表格中該方法所對應(yīng)的這一行,接著將會跳轉(zhuǎn)到下面的界面:

圖10

是不是很有趣,applyTonalFilter()是UIImage的一個擴展方法,幾乎100%的花費在它上面的時間都被用在了生成過濾后要輸出的圖片上。

要想提速這個過程,還真沒有什么好的辦法,畢竟創(chuàng)建圖片是一個連續(xù)的過程,要一直持續(xù)到其創(chuàng)建完畢。現(xiàn)在讓我們跳回一步看看applyTonalFilter()是在哪里被調(diào)用的。點擊代碼預(yù)覽區(qū)頂部的瀏覽路徑記錄中的Call Tree返回上一個界面看看:

圖10

現(xiàn)在點擊表格頂部的函數(shù)applyTonalFilter左側(cè)的小箭頭,這將展開Call Tree以顯示applyTonalFilter的上級調(diào)用者。你可能還需要展開下一行;當(dāng)對Swift語言做性能分析時,有時會在Call Tree中出現(xiàn)以@objc為前綴的重復(fù)行。你所感興趣的應(yīng)該第一行中以你的app名字為前綴的那個調(diào)用者(此處的前綴應(yīng)該是InstrumentsTutorial):

圖11

(PS:自己插一句,從這里也可以看出Swift語言和Object-C語言的關(guān)系,更好的理解在開發(fā)中二者為什么可以混編)

這種情況下,你可以看到該行涉及到結(jié)果集合界面中的函數(shù):cellForItemAtIndexPath。雙擊該行以查看工程中的相關(guān)代碼。

現(xiàn)在你可以看出到底是什么問題了。直接由函數(shù)cellForItemAtIndexPath調(diào)用的色調(diào)過濾方法執(zhí)行時占用了太長的時間,這樣一來每次請求一張過濾后的圖片時都會阻塞主線程(進而阻塞整個UI界面)。

進行分流的操作

要解決這個問題,你需要兩步走:第一,通過dispatch_async(異步線程函數(shù))函數(shù)將圖片過濾方法分流到后臺線程中;然后,緩存已經(jīng)生成的圖片。在樣例工程中已經(jīng)提供了一個輕量級的圖片緩存類(有一個引人注目的名字ImageCache),該類將圖片儲存到內(nèi)存中,然后在需要時通過一個鍵值再將圖片取出來。

你現(xiàn)在可以手動切換到Xcode看到你在Instruments中所看到的代碼,但是有一個更便捷的方法實現(xiàn)這一功能:點擊下圖紅色圈圈中的按鈕即可。

圖12

你可以看到,Xcode在非常精確的位置顯示出相應(yīng)的代碼。

現(xiàn)在, 在collectionView(_:cellForItemAtIndexPath:)中, 使用下面的代碼來替換對loadThumbnail()的調(diào)用:

flickrPhoto.loadThumbnail { image, error in

if cell.flickrPhoto == flickrPhoto {

if flickrPhoto.isFavourite {

cell.imageView.image = image

} else {

if let cachedImage = ImageCache.sharedCache.imageForKey("\(flickrPhoto.photoID)-filtered") {

cell.imageView.image = cachedImage

} else {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {

if let filteredImage = image?.applyTonalFilter() {

ImageCache.sharedCache.setImage(filteredImage, forKey: "\(flickrPhoto.photoID)-filtered")

dispatch_async(dispatch_get_main_queue(), {

cell.imageView.image = filteredImage

})

}

})

}

}

}

}

這段代碼的第一部分和先前相同,用來從網(wǎng)上加載Flickr相冊的縮略圖。如果圖片先前已經(jīng)被緩存過,那么cell顯示當(dāng)前的緩存的內(nèi)容,否則色調(diào)過濾器就會被調(diào)用來生成相應(yīng)的圖片。

那么哪些內(nèi)容發(fā)生改變了呢:首先,這里代碼會首先檢測緩存中是否已經(jīng)有存在的圖片,如果有,那好,圖片會被直接顯示出來。如果沒有,會對圖片調(diào)用色調(diào)過濾器代碼,并將這些代碼發(fā)送到后臺隊列處理。這樣在對圖片進行加工處理的同時,仍然能夠讓UI保持非常流暢的響應(yīng)。當(dāng)加工處理的操作進行完畢后,圖片被緩存,然后在主線程中更新圖片的顯示。

上述那些是已經(jīng)處理好的圖片,但是仍然有一些原始的Flickr縮略圖需要你去關(guān)注。打開FlickrSearcher.swift文件并找到loadThumbnail(_:)這個函數(shù)。用下面的函數(shù)替換掉該函數(shù)。

func loadThumbnail(completion: ImageLoadCompletion) {

if let image = ImageCache.sharedCache.imageForKey(photoID) {

completion(image: image, error: nil)

} else {

loadImageFromURL(URL: flickrImageURL(size: "m")) { image, error in

if let image = image {

ImageCache.sharedCache.setImage(image, forKey: self.photoID)

}

completion(image: image, error: error)

}

}

}

這和加工過濾圖片的代碼十分相似。

如果所需的一張圖片在緩存中已經(jīng)存在,那么completion這個閉包將會被立即調(diào)用。否則,圖片將會從Flickr加載然后加工處理后再儲存在緩存中。

通過Xcode中的菜單項Product\Profile,在Instruments中重新運行這個app(或者使用快捷鍵?+I )。

像前面說的那樣,執(zhí)行幾個搜索任務(wù),這次你會看到UI的體驗沒有像先前那么遲鈍了。圖片過濾器現(xiàn)在在后臺異步調(diào)用,圖片緩存也被放到了后臺,也就是說圖片僅僅是加工過濾一次,同時也僅僅被緩存一次。在Call Tree中你將看到大量的后臺工作線程,這些線程用來處理任務(wù)繁重的圖片處理工作。

一切看起來那么的美好,是時候上傳代碼了嗎?還沒有!

分配、分配、再分配

本課程中的下一個Instruments選項是Allocations。該選項會給出程序中創(chuàng)建的所有對象的詳細信息,以及它們使用內(nèi)存的情況,同時該選項還會呈現(xiàn)出每個對象的引用計數(shù)。

退出先前運行的app,重新啟動一個Instruments配置項。這次,編譯并運行app,然后在導(dǎo)航區(qū)域打開調(diào)試導(dǎo)航選項。接著點擊子選項Memory,在主窗口中顯示內(nèi)存使用情況的圖表。其圖表應(yīng)該和下面這張圖差不多:

圖13

這些圖表對于想快速了解你的app當(dāng)前的運行狀況是非常有用的。但是你或許需要更進一步。點擊窗口右上方的Profile in Instruments按鈕,然后該會話將自動被帶入到Instruments中,并且Instruments將自動打開Allocations選項。

圖14

這次你將注意到有兩個條目軌道。一個叫Allocations, 另一個叫Leaks。軌道Allocations將在稍后討論其細節(jié);軌道Leaks通常情況下在Objective-C中更有用,所以本課程不涉及這部分。

那么接下來你將追蹤什么Bug呢?

項目中可能有一些你不知道的東西隱藏在其中。你很可能聽說過內(nèi)存泄漏。但是或許你還不知道其實存在兩種類型的內(nèi)存泄漏:

1、True memory leaks(真正的內(nèi)存泄漏),存在于一個對象不再被使用,但是仍然占用已經(jīng)分配了的內(nèi)存——這意味著其所占用的內(nèi)存永遠不可能被重復(fù)利用,即使使用Swift和ARC幫忙管理內(nèi)存。最常見的內(nèi)存泄漏是retain cycle(循環(huán)持有)或者strong reference cycle(強引用循環(huán))。這種情況發(fā)生在兩個對象相互之間強引用,因此在析構(gòu)時發(fā)現(xiàn)兩個對象相互持有,這也就意味著它們所占用的內(nèi)存永遠都不可能被釋放,造成內(nèi)存泄漏。

2、Unbounded memory growth(無界內(nèi)存增長)?這種情況發(fā)生在內(nèi)存持續(xù)的分配卻從沒有機會釋放,如果任由這種情況持續(xù),然后在某個時間點上系統(tǒng)內(nèi)存將會被耗盡,此時你也必將面臨著該怎么處理一個大的內(nèi)存管理問題。在iOS系統(tǒng)環(huán)境中,這意味著你的app將會被系統(tǒng)干掉。

在Instruments的Allocations選項下運行app,在app中做5次不同的搜索,但是先不要對搜索結(jié)果進行點擊操作。在確保有搜索結(jié)果的情況下,現(xiàn)在等幾秒鐘,讓app獨自“靜靜”。

你或許已經(jīng)注意到了在軌道Allocations中的圖表內(nèi)容一直在增長,這是在告訴你內(nèi)存正在被分配。正是這種特性將引導(dǎo)著你找到“無界內(nèi)存增長”。

接下來你要執(zhí)行的操作是“內(nèi)存生成分析”(“generation analysis”)。要這么做,你只需點擊Mark Generation按鈕。如下圖所示:

圖15

點擊Mark Generation按鈕后,你將看到一面紅色的小旗出現(xiàn)在軌道中,就像下面這樣的:

圖16

“內(nèi)存生成分析”的目的是多次執(zhí)行一個動作,看看內(nèi)存是否會無限的增長。進入一個搜索結(jié)果,接著等幾秒鐘等待圖片加載完畢,然后返回主界面。然后再次執(zhí)行生成分析。對不同的搜索結(jié)果,重復(fù)執(zhí)行上述過程幾次。

反復(fù)點擊幾個搜索結(jié)果后,Instruments將會看起來是這樣的:

圖17

在這個時候,你應(yīng)該產(chǎn)生懷疑。注意一下,看看你每次進入搜索結(jié)果的時候藍色圖表的增長。這看起來確實不好,但是等等,我們都知道的內(nèi)存警告呢!內(nèi)存警告是iOS系統(tǒng)下的一個告知app內(nèi)存變得緊張,需要做內(nèi)存清理的一種方式。

有可能上述現(xiàn)象的產(chǎn)生不僅僅是你的app造成的,也有可能是系統(tǒng)UIKit庫中某個深層次的原因。在你指責(zé)它們之前,給系統(tǒng)框架或者你自己的app一個機會去清理內(nèi)存。

點擊Instruments菜單欄中的Instrument\Simulate Memory Warning項,或者模擬器菜單欄中的Hardware\Simulate Memory Warning項,來模擬一個內(nèi)存警告。你將觀察到內(nèi)存使用降了一點點或者根本就沒有降,圖表根本沒有變回到它該有的形式。因此我們可以斷定在某個地方仍有“無界內(nèi)存增長”產(chǎn)生了。

關(guān)于你每次瀏覽搜索結(jié)果后內(nèi)存就增長的原因,看一下詳情面板,你會看到一大堆分配內(nèi)存的條目。

探討每一次的標(biāo)記

在每次做標(biāo)記的過程中,你會看到所有的對象都被分配了內(nèi)存,到下次做標(biāo)記的時候,這些東西仍然存在。下次標(biāo)記只包含在上次標(biāo)記時出現(xiàn)的對象。

看看“Growth”這一列,你會看到在某處內(nèi)存明顯的增長了,打開某一次標(biāo)記,你將看到下面圖中的情況:

圖18

那么多的對象數(shù)據(jù),從哪里下手呢?

不幸的是,相比于Objective-C語言,Swift將該界面搞得非常的亂,其中填充了好多你不需要關(guān)注的內(nèi)部數(shù)據(jù)。你可以將Allocation Type切換到All Heap Allocations模式,以及點擊“Growth”列的頭部使數(shù)據(jù)按照數(shù)字大小排列內(nèi)容,這樣界面看起來會清晰一些。

在接近頂部的地方,有這么一行:ImageIO_jpeg_Data,這確實是app中的內(nèi)容,用來處理某些事物的東西。點擊ImageIO_jpeg_Data行左邊的箭頭顯示完整的列表。對下面展開的列表中選擇一行,點擊該行右側(cè)的向右的擴張箭頭(或者快捷鍵:?+3),可以查看其堆棧使用軌跡:

圖19

這顯示了特定對象被創(chuàng)建時堆棧的使用軌跡。堆棧軌跡中,灰色的部分是系統(tǒng)庫的內(nèi)容,黑色部分才是你的app代碼相關(guān)的部分。要為當(dāng)前的堆棧軌跡獲取更多的上下文信息,雙擊倒數(shù)第二行的黑色部分,也就是唯一的一個有著“InstrumentsTutorial”前綴的行,這表示這行內(nèi)容來自于Swift代碼。雙擊該行將跳轉(zhuǎn)到相應(yīng)的代碼:collectionView(_:cellForItemAtIndexPath:)。

Instruments確實很有用,但是其作用有限!現(xiàn)在你需要自己觀看代碼以找出問題的原因。

通覽整個方法,你會發(fā)現(xiàn)它正在調(diào)用setImage(_:forKey:),就像我們先前講述Time Profiler時提到的,這個函數(shù)是用來緩存圖片等,聽起來好像問題就出在這里!

點擊前面提到的“Open in Xcode”按鈕,跳回到Xcode中。打開ImageUtilities.swift文件看看方法setImage(_:forKey:)的實現(xiàn):

func setImage(image: UIImage, forKey key: String) {

images[key] = image

}

該函數(shù)將一張圖片加入到字典中進行緩存,但是仔細看代碼,你就會發(fā)現(xiàn)沒有任何操作會將該圖片從字典中刪除!

這就是“無限內(nèi)存增長”的原因——一直向緩存中添加?xùn)|西,卻從來沒有刪除緩存中的東西。

要修復(fù)這個問題,你需要讓ImageCache監(jiān)聽UIApplication發(fā)出的內(nèi)存警告通知,當(dāng)ImageCache收到這個通知時就會執(zhí)行清理緩存的操作。

要讓ImageCache監(jiān)聽UIApplication發(fā)出的內(nèi)存警告通知,向類中添加initializer和de-initializer兩個方法:

init() {

NSNotificationCenter.defaultCenter().addObserverForName(

UIApplicationDidReceiveMemoryWarningNotification,

object: nil, queue: NSOperationQueue.mainQueue()) { notification in

self.images.removeAll(keepCapacity: false)

}

}

deinit {

NSNotificationCenter.defaultCenter().removeObserver(self,

name: UIApplicationDidReceiveMemoryWarningNotification,

object: nil)

}

上述代碼前半部分,為UIApplicationDidReceiveMemoryWarningNotification通知注冊了一個觀察者,以執(zhí)行清理圖片的閉包函數(shù)。

上述代碼要做的是刪除緩存中的對象,這樣就確保了沒有任何地方再持有那些圖片,然后它們就可以被釋放了!

要測試修復(fù)情況,按照先前的步驟重新啟動Instruments。但是別忘了最后模擬幾次內(nèi)存警告看看效果如何。

注意:先關(guān)閉Instruments,在Xcode中,點擊菜單欄中的Product/Clean,對代碼進行清理,然后編譯運行,最后再運行Instruments。這樣確保使用的是最新的代碼。

這次“Mark Generation”的結(jié)果應(yīng)該是下面這樣的:

圖20

你可能已經(jīng)注意到了內(nèi)存的使用量在內(nèi)存警告后下降了,然而仍然有些部分的內(nèi)存是增長的,但是其增長量比先前少多了。

仍有小部分內(nèi)存增長的原因確實是因為系統(tǒng)庫造成的,當(dāng)然你對此也無能為力??雌饋硐到y(tǒng)庫沒有釋放所有的內(nèi)存,這可能是被設(shè)計成如此或者是個bug。你能做的就是在你的app中盡量多的釋放內(nèi)存,而且你已經(jīng)做到了。

干的好,又一個問題被解決了,但是現(xiàn)在還不是上傳代碼的時候,仍然有一些我們前面提到的第一類內(nèi)存泄漏問題沒有被定位到。

強循環(huán)引用

最后,你要開始著手找出程序的中的強循環(huán)引用了。如先前所提到的,強循環(huán)引用發(fā)生在兩個對象持有相互的強引用時,導(dǎo)致最后對象不能釋放,進而消耗內(nèi)存??梢岳肐nstruments中的Allocations以一種不同的方式偵測到這種循環(huán)。

注意:要想學(xué)習(xí)課程余下的部分,你必須讓app在真機上運行,而不是在模擬器上。

關(guān)閉Instruments,返回Xcode,連上真機,并確保app的編譯目標(biāo)選項為真機。再次選擇Product\Profile,接著選擇Allocations選項。

圖21

這輪調(diào)試中,你只需要關(guān)注在內(nèi)存中懸垂著的指針即可。你可能已經(jīng)注意到了,詳情面板上填滿了大量的對象——太多了導(dǎo)致很難全部瀏覽。

為了縮小范圍,只看自己感興趣的對象,在Allocations Summary右側(cè)的輸入框中輸入“Instruments”作為過濾詞,那么就會只顯示名稱中有此關(guān)鍵字的對象了。因為樣例app的名稱就叫 “InstrumentsTutorial”,所以現(xiàn)在顯示的都是本項目所定義的。這樣一來事情看起來就簡單了一些。

圖22

Instruments中的“# Persistent” 和 “# Transient”這兩列沒有什么作用?!?i># Persistent”這一列記錄的是當(dāng)前內(nèi)存中每種對象數(shù)量的計數(shù), “# Transient”這一列記錄的是曾經(jīng)存在但是現(xiàn)在已經(jīng)釋放的對象數(shù)量。Persistent對象消耗內(nèi)存,Transient對象所占的內(nèi)存已經(jīng)釋放。

你應(yīng)該可以看到一個ViewController實例——顯示當(dāng)前你看到的界面。另外還有AppDelegate實例,F(xiàn)lickr API客戶端實例。

返回到app中,執(zhí)行一個搜索,然后點擊搜索結(jié)果。你可以看到Instruments又顯示出很多其他的對象:當(dāng)解析搜索結(jié)果的時候,F(xiàn)lickrPhotos相冊被創(chuàng)建了,SearchResultsViewController和ImageCache也被創(chuàng)建了。ViewController實例仍然存在——此時導(dǎo)航控制器仍然需要用到它。

此時,點擊app中的后退按鈕——SearchResultsViewController對象應(yīng)該被從導(dǎo)航堆棧彈出——那么它應(yīng)該被釋放。但是在Allocations摘要中的“# Persistent”列仍然能看到其計數(shù)為1!問題來了,為什么會出現(xiàn)這種情況??

嘗試執(zhí)行兩次下述過程:執(zhí)行一次圖片搜索,然后點擊進入搜索結(jié)果,接著點擊后退按鈕。此時你可以看到有3個SearchResultsViewControllers對象存在?!事實情況是,這些視圖控制器對象仍然存在于內(nèi)存中意為著有其他對象對它們產(chǎn)生了強引用——看起來你的代碼中存在強循環(huán)引用。

圖23

在這種情況下,你要排查的問題的主要線索不僅來自現(xiàn)存的SearchResultsViewController對象,還有所有的SearchResultsCollectionViewCell對象。貌似強循環(huán)引用發(fā)生在這兩個類的實例對象之間。

不幸的是,在寫這篇文章時,某些情況下,Instruments為Swift輸出的信息仍然不是特別的有用——不是特別詳細或者明確。Instruments只能給出問題在哪里的一些提示,以及顯示出對象在什么地方被分配——接下來,還需要你自己去排查到底出了什么問題。

讓我們深入代碼中看看。把你的鼠標(biāo)移動到Category列中的InstrumentsTutorial.SearchResultsCollectionViewCell這一項上,然后點擊其右邊的小箭頭——跳轉(zhuǎn)到的下個界面,將向你展示所有在app運行中SearchResultsCollectionViewCell對象的分配情況——很多項內(nèi)容,每個都是由一次搜索結(jié)果產(chǎn)生的。

圖24

操作右邊的Inspector(檢查器),點擊其面板頂部右邊第三個按鈕,切換到Extended Detail選項。此時該Inspector將向你展示當(dāng)前選中的對象的內(nèi)存分配過程中堆棧使用軌跡。和前面介紹的堆棧使用軌跡情況相同。堆棧軌跡中黑色的部分是和你的代碼相關(guān)的。雙擊最頂部的黑色行(以“InstrumentsTutorial”開頭的那一行)看看cell是在哪里分配的。

所有的cell都是在方法collectionView(cellForRowAtIndexPath:)開始時被分配的,如果你多向下瀏覽幾行代碼,你會看到下面的情況:

cell.heartToggleHandler = { isStarred in

self.collectionView.reloadItemsAtIndexPaths([ indexPath ])

}

這是處理集合視圖中每個cell上的心形按鈕的點擊動作的閉包。強循環(huán)引用就是在這里發(fā)生的——但是很難發(fā)現(xiàn),除非你以前遇到過這種情況。

The closure cell refers to theSearchResultsViewControllerusingself, which creates a strong reference. The closurecapturesself. Swift actually forces you to explicitly use the wordselfin closures (whereas you can usually drop it when referring to methods and properties of the current object). This helps you be more away of the fact you’re capturing it. TheSearchResultsViewControlleralso has a strong reference to the cells, via their collection view.

閉包單元通過使用self引用了SearchResultsViewController,造成強引用。閉包 “捕捉” 到了self。在實際使用中,Swift語言強迫你在閉包中明確的使用關(guān)鍵字self。SearchResultsViewController通過它們的collection view也對cell產(chǎn)生了強引用。(這是對上一段英文的翻譯,抱歉有部分內(nèi)容不知道該怎么翻譯比較好,所以貼出英文原文。)

要破除強循環(huán)引用,你可以定義一個“捕獲列表?”作為閉包定義的一部分。捕獲列表可以用來聲明那些被閉包捕獲的對象實例,這些實例可以被聲明為weak或者unowned類型。

Weak:當(dāng)對某個對象的引用在未來的某個時間點有可能變?yōu)閚il的時候應(yīng)該使用weak關(guān)鍵字。如果被引用的對象被釋放,引用變?yōu)閚il。就其本身而論,weak是可選的。

Unowned:當(dāng)閉包和其引用的對象相互之間總是有著相同的生命周期時,并且也是同時被釋放時,使用Unowned關(guān)鍵字。一個unowned類型的引用絕對不會變成nil。

要修強循環(huán)引用的問題,點擊Open in Xcode按鈕,打開SearchResultsViewController.swift文件,為閉包heartToggleHandler添加一個捕獲列表。

cell.heartToggleHandler = { [weak self] isStarred in

if let strongSelf = self {

strongSelf.collectionView.reloadItemsAtIndexPaths([ indexPath ])

}

}

將self聲明為weak類型,意味著即使collection view cell有指向SearchResultsViewController對象的引用,SearchResultsViewController對象也可被釋放,因為它們之間是弱引用的關(guān)系。SearchResultsViewController的釋放會接著引發(fā)對collection view的釋放,接著是對collection view cell的釋放。

從Xcode內(nèi)部,使用快捷鍵?+I再一次編譯并在Instruments中運行app。

在Instruments中使用Allocations選項,按照你先前的做法(記住過濾掉一些內(nèi)容)。執(zhí)行一個搜索,點擊進入搜索結(jié)果界面,然后再返回。你應(yīng)該看到當(dāng)從導(dǎo)航返回時,SearchResultsViewController和其cell都被釋放了。

終于,強循環(huán)引用的問題也被解決了。好了,可以上傳代碼了。

從這里出發(fā),要到哪里去?

這里是使用Instruments改進優(yōu)化后的代碼。

現(xiàn)在你已經(jīng)學(xué)會上述知識了,去使用Instruments調(diào)試你的代碼,看看會發(fā)生什么有趣的事情。當(dāng)然,努力讓使用Instruments優(yōu)化代碼成為你工作流程的一部分。

你應(yīng)該經(jīng)常使用Instruments檢查你的代碼,在發(fā)布app之前盡量清除掉內(nèi)存管理和性能上的問題。

現(xiàn)在,去著手制作一些高效而了不起的app吧!


注:

1、在原文中好多地方,作者有的地方method,有的地方用function,其實表達的是同一個意思,不管是理解成“方法”也好、“函數(shù)”也罷,都不影響對整體意思的把握。

2、翻譯時,文中的某些英文單詞在不影響閱讀和理解的情況下并沒有翻譯——畢竟好多東西在業(yè)界沒有統(tǒng)一的叫法——在意思相同的情況下,每個人按照自己的理解,可能更便于閱讀理解。

3、原作者是個很有趣的人,所以文中口語化比較多,語法也不是很嚴(yán)謹(jǐn),所以翻譯力求順暢的傳達意思,而不是逐字逐句的翻譯——所謂的意譯。

4、建議有興趣的讀者去看看英文原版,畢竟翻譯過來的都是二手信息,難免會有不足之處。

要閱讀英文原文,請點擊:原文鏈接。

5、翻譯講究信達雅,談不上雅,但希望自己能做到信。水平有限,如有紕漏,還請各位大神多多指教,謝謝。

6、原創(chuàng)翻譯,尊重他人的勞動成果,若要轉(zhuǎn)載,請注明本文鏈接,謝謝。


后記:翻譯能讓自己的英語水平快速的突飛猛進,同時也可以鍛煉自己的意志——尤其是比較長的文章,很折磨人的——光打字都夠你受得了,哈哈。也能讓你學(xué)習(xí)新的東西,同時反復(fù)的核對內(nèi)容的過程,能讓知識點記得更牢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容