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

圖1

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

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

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

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

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

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

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

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

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

注意:在這篇課程中撩炊,我們假設(shè)你對Swift語言和iOS編程已經(jīng)相當?shù)氖煜ね庥馈5侨绻闶莻€iOS編程方面的新手,你或許還需要瀏覽一下該網(wǎng)站上的其他內(nèi)容拧咳。本課程使用到了storyboard(故事板)伯顶,所以請確保你已經(jīng)熟悉storyboard這個東西。如果不熟悉骆膝,請瀏覽這個鏈接祭衩。

準備好了嗎?我們即將開始深入探索迷人的Instruments世界了阅签。

正式開始了:

在此掐暮,你不需要為此課程從頭開始創(chuà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

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

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

時間性能檢測

圖3

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

這里是一張Time Profiler運行時的預覽圖:

圖4

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

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

注意:一般情況下,你總應該在真機上檢測你的程序源哩,而不是在模擬器上鞋吉。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分析其他進程——不過不用擔心酬土,在此提供密碼是安全的。

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

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

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

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

圖6

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

圖7

1岂昭、這里是錄制控制部分以现,當你點擊紅色的“錄制”按鈕時,將開啟或關(guān)閉當前正在監(jiān)測的app(當該按鈕被觸發(fā)時约啊,其狀態(tài)在“錄制”和“停止”之間切換)邑遏。“暫颓【兀”按鈕暫停當前app的執(zhí)行记盒。

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

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

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

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

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

深入了解

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

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

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

在右手邊,選擇Display Setting檢查器(或者快捷鍵?+2)茫多。在檢查器窗口中祈匙,在Call Tree選項下面, 選擇Separate by Thread天揖、Invert Call Tree夺欲、Hide Missing SymbolsHide System Libraries。操作完成后宝剖,界面看起來應該是這樣的:

圖8

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

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

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

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

4汁展、Hide System Libraries: 當這個選項被勾選時,只有你自己app中的方法名會被顯示厌殉。通常情況下食绿,勾選這個選項還是很有用的,因為你只會關(guān)注自己代碼所使用的CPU時間——當然你不會關(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ù)則不會被顯示芳撒。當你的程序中沒有C、C++類型的函數(shù)時未桥,該選項沒有什么作用笔刹,但是如果我們正在運行的是一個OpenGL app,其中很可能會有一些C++函數(shù)冬耿,此時該選項就可以發(fā)揮做用了舌菜。

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

圖9

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

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

圖10

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

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

圖10

現(xiàn)在點擊表格頂部的函數(shù)applyTonalFilter左側(cè)的小箭頭劳殖,這將展開Call Tree以顯示applyTonalFilter的上級調(diào)用者。你可能還需要展開下一行拨脉;當對Swift語言做性能分析時哆姻,有時會在Call Tree中出現(xiàn)以@objc為前綴的重復行。你所感興趣的應該第一行中以你的app名字為前綴的那個調(diào)用者(此處的前綴應該是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在非常精確的位置顯示出相應的代碼性誉。

現(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顯示當前的緩存的內(nèi)容煌往,否則色調(diào)過濾器就會被調(diào)用來生成相應的圖片倾哺。

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

上述那些是已經(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í)行幾個搜索任務恬偷,這次你會看到UI的體驗沒有像先前那么遲鈍了。圖片過濾器現(xiàn)在在后臺異步調(diào)用诡延,圖片緩存也被放到了后臺,也就是說圖片僅僅是加工過濾一次惹恃,同時也僅僅被緩存一次。在Call Tree中你將看到大量的后臺工作線程参淹,這些線程用來處理任務繁重的圖片處理工作。

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

分配伤极、分配、再分配

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

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

圖13

這些圖表對于想快速了解你的app當前的運行狀況是非常有用的齿穗。但是你或許需要更進一步。點擊窗口右上方的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)存永遠不可能被重復利用,即使使用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)存正在被分配姆钉。正是這種特性將引導著你找到“無界內(nèi)存增長”说订。

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

圖15

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

圖16

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

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

圖17

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

有可能上述現(xiàn)象的產(chǎn)生不僅僅是你的app造成的坪圾,也有可能是系統(tǒng)UIKit庫中某個深層次的原因晓折。在你指責它們之前,給系統(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)存的條目护奈。

探討每一次的標記

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

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

圖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)的部分朗和。要為當前的堆棧軌跡獲取更多的上下文信息,雙擊倒數(shù)第二行的黑色部分簿晓,也就是唯一的一個有著“InstrumentsTutorial”前綴的行眶拉,這表示這行內(nèi)容來自于Swift代碼。雙擊該行將跳轉(zhuǎn)到相應的代碼: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)存增長”的原因——一直向緩存中添加東西,卻從來沒有刪除緩存中的東西孵奶。

要修復這個問題疲酌,你需要讓ImageCache監(jiān)聽UIApplication發(fā)出的內(nèi)存警告通知,當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ù)僻肖。

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

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

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

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

圖20

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

仍有小部分內(nèi)存增長的原因確實是因為系統(tǒng)庫造成的,當然你對此也無能為力挎袜⊥缒簦看起來系統(tǒng)庫沒有釋放所有的內(nèi)存,這可能是被設(shè)計成如此或者是個bug盯仪。你能做的就是在你的app中盡量多的釋放內(nèi)存紊搪,而且你已經(jīng)做到了。

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

強循環(huán)引用

最后,你要開始著手找出程序的中的強循環(huán)引用了奔浅。如先前所提到的馆纳,強循環(huán)引用發(fā)生在兩個對象持有相互的強引用時,導致最后對象不能釋放汹桦,進而消耗內(nèi)存鲁驶。可以利用Instruments中的Allocations以一種不同的方式偵測到這種循環(huán)舞骆。

注意:要想學習課程余下的部分钥弯,你必須讓app在真機上運行径荔,而不是在模擬器上。

關(guān)閉Instruments脆霎,返回Xcode总处,連上真機,并確保app的編譯目標選項為真機睛蛛。再次選擇Product\Profile鹦马,接著選擇Allocations選項。

圖21

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

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

圖22

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

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

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

此時挫掏,點擊app中的后退按鈕——SearchResultsViewController對象應該被從導航堆棧彈出——那么它應該被釋放侦另。但是在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只能給出問題在哪里的一些提示,以及顯示出對象在什么地方被分配——接下來莹捡,還需要你自己去排查到底出了什么問題鬼吵。

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

圖24

操作右邊的Inspector(檢查器)涣脚,點擊其面板頂部右邊第三個按鈕,切換到Extended Detail選項寥茫。此時該Inspector將向你展示當前選中的對象的內(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:當對某個對象的引用在未來的某個時間點有可能變?yōu)閚il的時候應該使用weak關(guān)鍵字。如果被引用的對象被釋放敌蚜,引用變?yōu)閚il桥滨。就其本身而論,weak是可選的弛车。

Unowned:當閉包和其引用的對象相互之間總是有著相同的生命周期時齐媒,并且也是同時被釋放時,使用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é)果界面,然后再返回弊决。你應該看到當從導航返回時噪舀,SearchResultsViewController和其cell都被釋放了魁淳。

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

從這里出發(fā)纺座,要到哪里去息拜?

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

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

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

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


注:

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

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

3挑辆、原作者是個很有趣的人,所以文中口語化比較多孝情,語法也不是很嚴謹鱼蝉,所以翻譯力求順暢的傳達意思,而不是逐字逐句的翻譯——所謂的意譯箫荡。

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

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

5睬魂、翻譯講究信達雅终吼,談不上雅,但希望自己能做到信氯哮。水平有限,如有紕漏商佛,還請各位大神多多指教喉钢,謝謝。

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


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末击狮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子益老,更是在濱河造成了極大的恐慌彪蓬,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捺萌,死亡現(xiàn)場離奇詭異档冬,居然都是意外死亡,警方通過查閱死者的電腦和手機互婿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門捣郊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人慈参,你說我怎么就攤上這事呛牲。” “怎么了驮配?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵娘扩,是天一觀的道長着茸。 經(jīng)常有香客問我,道長琐旁,這世上最難降的妖魔是什么涮阔? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮灰殴,結(jié)果婚禮上敬特,老公的妹妹穿的比我還像新娘。我一直安慰自己牺陶,他們只是感情好伟阔,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掰伸,像睡著了一般皱炉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狮鸭,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天合搅,我揣著相機與錄音,去河邊找鬼歧蕉。 笑死灾部,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的廊谓。 我是一名探鬼主播梳猪,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蒸痹!你這毒婦竟也來了春弥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叠荠,失蹤者是張志新(化名)和其女友劉穎匿沛,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榛鼎,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡逃呼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了者娱。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抡笼。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖黄鳍,靈堂內(nèi)的尸體忽然破棺而出推姻,到底是詐尸還是另有隱情,我是刑警寧澤框沟,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布藏古,位于F島的核電站增炭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拧晕。R本人自食惡果不足惜隙姿,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厂捞。 院中可真熱鬧输玷,春花似錦、人聲如沸蔫敲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奈嘿。三九已至,卻和暖如春吞加,著一層夾襖步出監(jiān)牢的瞬間裙犹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工衔憨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叶圃,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓践图,卻偏偏與公主長得像掺冠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子码党,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容