本文由Mr_cyz(博客)翻譯自raywenderlich,歡迎參與咱們的翻譯活動。
原文:Instruments Tutorial with Swift: Getting Started
php
不管你寫過許多iOS應用,仍是剛剛開始你的第一個應用,毫無疑問,你都會想出一些新點子,或者想去弄明白你該怎麼作,來讓你的app變得更好。 html
除去添加新特性來優化你的應用,有一件事是全部好的開發者都回去作的,那就是診斷他們的代碼。 swift
該教程將向你展現怎麼樣去使用Xcode提供的工具"Instrument"中最重要的一些功能。幫助你檢查本身代碼中的性能問題、內存管理問題、循環引用問題以及其餘種種。 api
在本篇教程中,你將學到: xcode
怎樣使用Time Profiler工具來定位你的代碼中的"高消耗點(hot-spot)",從而讓你的代碼更加有效率。 緩存
怎樣使用Allocations工具來檢測和改正代碼中的內存管理問題,例如循環強引用。 安全
注意:本教程假定你已經上手了iOS開發和swift語言。若是你是iOS開發的初學者,你可能更適合去看一下本網站上的其餘教程。本篇教程還使用了storyboard,因此確保你熟悉相關概念。本網站上的這篇教程是一個很好的起點。
(編輯注:若是你想全面瞭解Instruments,請參看:Instruments 用戶指南【中文完整翻譯版】) 網絡
一切就緒?準備好進入instrument的迷人的世界中吧。 數據結構
起步 閉包
在本篇教程中,你無需從頭開始建立一個完整的應用,咱們已經爲你提供了一個示例程序,你的任務是瀏覽這個應用,而後使用instrument做爲你的助手來改善這個應用--相似於你優化本身的應用的過程。
從這裏下載starter project,解壓後使用Xcode打開。
該示例程序使用Flickr提供的API來搜索圖片。你須要一個API key來使用這個API。對樣例程序而言,你能夠去Flickr的網站上建立一個樣例key,而後就能夠經過網站http://www.flickr.com/services/api/explore/?method=flickr.photos.search 來搜索圖片,並使用時把API key拷貝到上述url的最後面,格式爲"&api_key=",接下來的參數一樣加到&後面。
例如,若是URL是http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=6593783efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f ,那麼API key就是6593783efea8e7f6dfc6b70bc03d2afb。
把這個key粘貼到FlickrSearcher.swift文件頂部,取代原有的key。
須要注意的是,該key每隔一天左右都會改變,因此你可能碰巧須要去從新生成一個key。若是key不可用了,你的應用將會提醒你。
編譯並運行應用,執行一次查詢,而後點擊一個結果,你將會看到相似下面的界面。
瀏覽一下這個應用,弄清楚基本的功能,你可能會想,一旦UI看起來不錯後,這個應用就準備好上傳了。然而,接下來你將看到使用Instruments工具後將爲你的app帶來多少好處。
本教程剩下的內容將會向你展現怎麼樣找到並改正存在於你的應用中的問題。你將看到Instruments工具怎麼樣使debug程序的工做變得易如反掌。
時間分析儀
首先你將使用的工具是Time Profiler。在每一個測量時間間隔內,該工具將暫停程序執行,在每一個線程上進行一次棧追蹤(stack trace),能夠想象成點了Xcode調試工具中的暫停鍵。
這裏有一張Time Profiler的預覽圖。
這個界面展現的是調用樹(call tree)。調用樹展現的是一個app中執行不一樣的方法花費的時間,每一行都是程序執行路徑中的一個不一樣的方法,每一個方法花費的時間能夠由分析工具在其中暫停的次數來決定。
例如,若是有100件事情要作,每件花費1毫秒,在棧頂的方法作了其中10件,那麼你能夠推斷出,大約在總執行時間中的10%--10毫秒--花費在了這個方法中。這是至關粗糙的估計,但確實有效!
注意:一般來講,你應該老是在真機上分析你的app,而不是在模擬器上。iOS模擬器有你的Mac提供的性能支撐,可是真機做爲硬件移動設備,資源是有限的。因此你的app可能在模擬器上運行得很好,可是一旦它運行到真機上,你可能就會發現有性能問題。
那麼馬上開始分析吧。
從Xcode的菜單欄中,選擇product/profile,或者按下commond+I,這時會編譯程序,加載Instruments工具,而後會出現一個選擇框,相似於下面的圖片:
Instruments提供了不一樣的模板。
選擇Time Profiler工具,而後點擊Choose,這時會出現一個新的工具文件。點擊左上角的紅色記錄按鈕,開始記錄並加載你的app,你可能須要輸入密碼來爲Instruments分析其餘進程受權--不用擔憂,這很安全。
在Instruments窗口中,能夠看到一個計時器,還有一個小箭頭在屏幕中央的圖表上從左向右移動。這代表app正在運行。
如今開始使用這款app,搜索圖片,而後點擊幾個查詢結果進入詳情界面,你可能會發現進入一個詳情界面很是慢,另外滑動查詢結果的列表也是慢得難以置信--這是一款笨重的app。
然而,你是幸運的,由於接下來你就會修正這一問題。不過在這以前你要先快速瀏覽一下當前展現的這個Instruments的界面。
首先,確保右手邊工具欄上的視圖選擇器的每個選項都被選中,以下:
這樣就確保全部的面板都被打開。如今看一下下面的截圖和每一部分的說明。
一、這裏控制記錄過程,點擊紅色的"記錄"按鈕能夠中止或開始當前正在分析的app(在記錄和中止按鈕之間切換),暫停鍵,如你所想,暫停當前正在運行的app。
二、這裏是執行計時器(run timer),計時器記錄着正在分析的app執行了多長時間、執行了多少次。若是你使用記錄控制按鈕來中止你的app,而後重啓,這將建立一個新的運行記錄,同時會顯示"Run 2 of 2"。
三、這裏被稱做路徑(track),就你選擇的Time Profiler工具而言,由於只有一個工具,因此這裏只有一條路徑,關於這裏顯示的圖標的詳情,一會你就會在接下來的教程中瞭解更多。
四、這裏是詳情面板,展現的是你正在使用的工具的主要信息。就如今而言,這裏展現的是最"笨重(hottest)"的方法--換句話說,佔用CPU時間最長的方法。點擊上方的bar會看到Call Tree(左手邊的那個)並選中Sample List,而後你會看到數據的不一樣視圖。視圖展現了每個示例。點擊其中幾個,你會在Extended Detail inspector中看到被捕獲的堆棧跟蹤。
五、這裏是檢查器(inspector)面板,一共有三個檢查器:record setting(記錄設置),display setting(展現設置),還有extends detail(擴展詳情)。一會你將瞭解更多關於這裏面的一些選項。
如今開始診斷這笨重的UI!:]
更進一步
搜索一次圖片,而後點擊結果進入詳情界面,我我的喜歡搜索"狗",不過選一個你喜歡的就好--你多是想搜索貓的一員:]
如今連續上下滾動列表數次,這樣你就在Time Profile工具中獲得足夠的數據了,能夠發現屏幕中央的數字在改變,圖表也開始被填充,這說明正在佔用CPU循環。
你固然不但願任何UI如此笨重,那麼table view就絕對不會被忽略,除非它滾動起來很是流暢。
要定位這裏的問題,你須要設置一些選項。
在右手邊,選擇display setting(或者按下commond+2),在該選擇器中,在Call Tree欄下選中Separate by Thread, Invert Call Tree, Hide Missing Symbols 和 Hide System Libraries選項,你的界面應該看起來是這樣的:
下面解釋了每個選項對左側列表中數據的顯示起了什麼做用:
Separate by Thread:每一個線程被單獨考慮。這能讓你知道哪個線程佔用CPU最多。
Invert Call Tree:選中該選項後,調用棧會自上至下顯示。這一般是你須要的,由於你想知道CPU花費時間的那個最深的方法。
Hide Missing Symbols:若是在你的app或者框架中找不到dSYM文件,那麼你將只能在列表中看到二進制代碼中的十六進制地址值,而不是方法的名稱(符號)。選中該選項後,只有能被解析的符號能夠被顯示出來,未被解析的十六進制數值會被隱藏,這有助於清理顯示的數據。
Hide System Libraries:選中該選項後,只有你本身app中出現的符號會被顯示出來。一般選中該選項是有用的,由於你只關心CPU在你本身的代碼中的哪一部分花費時間,你無法對系統庫使用CPU作多少改變。
Flatten Recursion:該選項將每個調用棧中的遞歸函數(調用它們自身的函數)視做單一入口,而不是多入口。
Top Functions:選上這一選項讓Instruments將花費在一個函數中的總時間視做在該函數中直接花費的時間加上調用的其餘函數花費的時間。因此若是函數A調用了函數B,那麼函數A花費的總時間被記爲A花費的時間加上B花費的時間。這一選項很是有用,由於它能讓你在每次進入調用棧時找到花費最長的時間,瞄準你最耗時的方法。
若是你正在使用Objective-C寫的app,那麼這裏還有一個選項:Show Obj-C Only,選擇該選項後,只展現Objective-C方法,不展現其餘任何C或C++的函數。目前你的app中沒有C或C++函數,可是舉例來講,若是你正在看的是一款OpenGL應用,那麼可能會有一些C++的函數。
儘管一些值可能會有輕微的不一樣,不過若是你選中了上面提到的幾個選項後,列表中展現的入口的順序應該是相似於下圖的:
額,這看起來不怎麼好,大量的時間被花在設置縮略圖的"色調"濾鏡('tonal'filter)的方法上了。這應該不會太讓你驚訝,由於列表的加載與滾動是UI中最笨重的部分,而這裏正式列表單元格被持續加載的地方。
爲了解到更多關於這個方法作了什麼的信息,雙擊列表中的這一行,這樣將把你帶到下面的視圖中:
這頗有趣,不是嗎?applyTonalFilter()是一個UIImage擴展中的一個方法,幾乎100%的時間被花費在這個方法中的應用圖片濾鏡後建立CGImage輸出這一地方了。
咱們沒辦法爲這一過程加速,建立一張圖片是個費時的過程。讓咱們回退一步,看看applyTonalFilter()是從哪裏調用的。點擊代碼界面的頂部欄中的Call Tree,回到上一界面。
而後點擊列表頂部applyTonalFilter左側的小箭頭,這樣就展開了Call Tree,展現出applyTonalFilter的調用者。你可能須要再展開到下一行。當你分析的是swift代碼時,有時在Call Tree中會出現重複的一行,以@objc爲前綴,此時你只須要關心第一行,以你的app的target名稱爲前綴(本例爲InstrumentsTutorial)。
這裏,該行代指collection view的cellForItemAtIndexPath方法的結果,雙擊該行能夠看到工程中相關的代碼。
如今你知道問題出在哪了。應用色調濾鏡的方法佔用了較長的時間,而該方法又直接從cellForItemAtIndexPath中調用,這樣每當該方法要求一個被濾鏡渲染的圖片時都會會阻塞主線程(整個UI)。
卸下重任
要解決這一問題,能夠分兩步來:首先使用dispatch_async將建立濾鏡的方法放到後臺線程,接着在每一張圖片被建立後都緩存起來。咱們的工程中有一個簡單的圖片緩存類(有一個易記的名字:ImageCache),簡單地將圖片保存到內存中,而後經過給定的鍵來獲取它們)。
如今能夠切換到Xcode上,手動找到當前你正在Instruments中看的源文件,不過如今在你的眼前,右側就有一個快捷按鈕Open in Xcode,在面板的代碼部分的上面找到它並點擊:
這樣,Xcode就定位到正確的位置了。
接下來,在collectionView(_:cellForItemAtIndexPath:)方法中,把調用loadThumbnail()方法替換爲下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
})
}
})
}
}
}
}
|
這段代碼的第一部分和以前同樣,從網絡上加載Flickr的圖片,若是該圖片被渲染過,那麼cell直接展現相應的縮略圖,若是沒有被渲染過,就將色調濾鏡應用到圖片上。
接下來就是改變的地方,首先代碼檢查圖片的濾鏡是否存在於圖片緩存中,若是是,那麼直接交由image view展現,若是沒有,那麼爲圖片添加色調濾鏡的方法被分配到後臺隊列中執行,當該濾鏡被渲染好之後,將渲染後的圖片保存到緩存中,在主線程中讓image view顯示圖片。
這樣就解決了須要濾鏡的圖片的問題,不過還須要考慮從Flickr請求下來的本來的縮略圖。打開FlickrSearcher.swift,找到loadThumbnail(_:),將其替換爲:
1
2
3
4
5
6
7
8
9
10
11
12
|
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)
}
}
}
|
這裏與處理濾鏡圖片相似,若是一張圖片已經存在於緩存中,那麼直接用緩存的圖片來調用completion回調,不然從Flickr上請求圖片並保存到緩存中。
經過Product/Profile(或者commond+I,記住,這些快捷鍵能夠節省你大量時間)打開Instruments,從新運行app。
能夠發現這一次你不須要選擇使用哪一個工具,由於你的app仍然在一個窗口中打開着,Instruments假定你想以一樣的選項再次運行。
進行幾回搜索,能夠發現此次UI不是那麼慢了,如今圖片濾鏡是異步渲染,圖片也在後臺被緩存,因此它們只須要被渲染一次,能夠在Call Tree中看到幾個dispatch_worker_threads,這裏是處理繁重的加載圖片濾鏡的過程。
看起來不錯,是時候作一次跨越了:]
分配、分配、分配
本教程要介紹的下一個工具是Allocations工具,它能夠給你關於全部被建立的對象和它們背後使用的內存的詳細信息。它也能顯示出每一個對象的引用計數。
要打開一個新的分析工具,首先退出Instruments工具。此次,編譯並運行app,在導航欄中點開Debug欄,而後點擊Memory就能夠在主窗口中顯示內存的使用圖表。
這些圖表能夠幫你大致上瞭解你的app的表現,不過你須要更強大的功能。點擊Profile in Instruments按鈕,而後能夠把這部分轉換到Instruments中。Allocations工具會自動打開。
此次你須要注意兩個追蹤,第一個叫作分配(Allocations),第二個是泄露(Leaks),分配追蹤將在下文詳細討論,一般泄露追蹤在Objective-C中更有用,因此本篇教程不會涉及。
那麼接下來你將去查找哪一個bug呢?
有些事被隱藏在工程中,你可能不知道它的存在。你可能據說過內存泄露,但不知道其實有兩種泄露:
一、"真正的內存泄露(True memory leaks)"是指一個對象再也不被引用但卻沒有被釋放--這說明內存永遠不能被複用,即便有swift和ARC幫助管理內存,最多見的內存泄露問題是保留環,或稱爲強引用環。當兩個對象互相持有對方的強引用時,每一個對象保證另外一個不會被釋放,這樣它們的內存將永遠不能被釋放!
二、"無限內存增加(Unbounded memory growth)"是指內存持續被分配而沒有機會被釋放。若是這一現象永遠持續下去,某一點上系統資源將被佔滿,這樣你就親手建立了一個大的內存問題。在iOS上意味着你的app將被系統殺死。
Allocations工具運行在app上時,進行五次不一樣的搜索,但不要點進詳細界面,確保每次搜索都有一些結果,如今讓app靜止等待幾秒鐘。
你應該能注意到Allocations追蹤中的圖表一直在增加,這說明內存正在被分配,這一特色將指導你找到無限內存增加問題。
接下來你要執行"分配分析(generation analysis)",要作到這一點,點擊Mark Generation按鈕,你能夠在Display Setting檢查器的頂部找到這一按鈕。
按下它,你將會發現一個紅旗出如今追蹤中,以下:
分配分析的目的是屢次執行一個事件,查看內存是否以無限的形式增加,點擊進入搜索的詳情界面,等待幾秒鐘的圖片加載,而後返回主頁,再一次mark generation,對於不一樣的搜索重複幾回這樣的操做。
在進入幾回詳情界面之後,Instruments將看起來以下圖所示:
這時你應該會有所起疑,能夠注意到每次搜索並進入詳情界面後藍色的圖表都在增加,這樣確定很差。不過等一下,內存警告呢?你應該知道的,內存警告是iOS告訴app內存緊缺的一種方式,並通知你你須要清理一些內存。
有可能這種增加不只僅是你的app形成的,它多是UIKit內部使用內存的結果。因此在指定具體哪個出現問題以前,給系統框架和你的app一個機會來清理本身的內存。
能夠在Instruments的菜單欄中選擇Instrument\Simulate Memory Warning來模擬一次內存警告,或者從模擬器的菜單欄中選擇Hardware\Simulate Memory Warning。你會注意到內存使用圖下陷了一點,也可能根本沒有。很顯然使用圖沒有回到應該的位置上,所以你的程序的某處依然有無限內存增加的問題。
每次點入詳情界面後都作一次標記的緣由是,你能夠看到在每一個標記段之間哪些內存被分配了。看一眼詳情面板,你會發現有大量的內存分配。
漫談分配
在每個generation段中,你能夠看到全部自標記以來被分配了內存空間,而且一直存活的對象。隨後的每一個generation段中只包含自上一個標記以後的符合上述描述的對象。
看一眼Growth欄,你就會發現確定在某處存在着增加問題,展開其中一個generation,你會看到以下圖界面:
哇,有好多的對象,咱們從哪開始呢?
很不幸,在這一界面上swift比Objective-C雜亂得多,由於這裏充滿了你並不須要瞭解的內部數據結構。你能夠經過切換Allocation Type至All Heap Allocations來方便地清除掉它們。固然也能夠點擊頂部的Growth頭,讓對象按照大小排序。
最頂部的對象是ImageIO_jpeg_Data,而且這確定是你的app創造的對象。點擊ImageIO_jpeg_Data左側的箭頭展開詳情列表,選中一行,而後打開Extended Detail檢查器(或者按下commond+3)。
這裏顯示的是當指定對象被建立時的棧追蹤,灰色部分的屬於系統框架,黑色部分是你的app中的。要了解這一追蹤的更多信息,雙擊黑色部分倒數第二行,這是惟一以InstrumentsTutorial開頭的一行,表明它是來自swift代碼的。雙擊它會把你帶到相關方法的代碼界面--你的老朋友collectionView(_:cellForItemAtIndexPath:)。
Instruments很是有用,可是這裏它不能幫你更多了,如今你必須親自瀏覽一遍代碼來了解這裏到底發生了什麼。
看一遍代碼,你會發現它調用了setImage(_:forKey:)方法,正如你在Time Profiler中看到的,這個方法緩存圖像以便以後在app中複用。啊哈,聽起來就像一個問題。
再次點擊Open in Xcode跳入Xcode界面,打開ImageUtilities.swift,看一下setImage(_:forKey:)的實現:
1
2
3
|
func setImage(image: UIImage, forKey key: String) {
images[key] = image
}
|
這裏以Flickr的圖片ID做爲鍵,將圖片保存到字典中。可是若是你總體瀏覽一遍代碼,你會發現圖片永遠不會從字典中被清除。
這就是你的無限內存增加的來源:全部事情都按照設定來工做,可是app永遠不會清除緩存--它只是不斷地往裏增長。
要解決這一問題,你須要作的是讓ImageCache監遵從UIApplication發來的內存警告的通知。當它收到通知後就清除掉它的緩存。
要讓ImageCache監聽通知,在該類中添加init和deinit方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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的觀察者來執行上面的閉包,清除圖片緩存。
代碼須要作的就是移除緩存中的全部對象,這樣就確保這些圖像再也不佔有什麼資源,它們將被釋放掉。
爲了測試這一修改,再次啓動Instruments(在Xcode中按下快捷鍵commond+I),重複以前的步驟,別忘了最後模擬一次內存警告。
注意:確保你是從Xcode中啓動並通過編譯,而不是僅僅按下Instruments中的紅色按鈕,這樣才能確保你使用的是最新的代碼。你也可能須要在進行分析以前先編譯運行一次,由於有時若是你直接分析,那麼Xcode彷佛沒有將模擬器中的app編譯更新到最新代碼上。
這一次的分配分析應該看起來是這樣的:
能夠發如今內存警告以後內存的使用下跌了。整體上依然有不少內存增加,可是不像以前那樣多了。
如今依然有不少內存增加是由系統庫形成的,而且你也無法對其作一些改進。這些系統庫並無釋放它們的所有內存,這有多是刻意設計的,也有多是一個bug。你能對你的app作的就是儘量多地釋放內存,而這一點你已經作到了! :]
很是好!又解決了一個問題!是時候進行新的跨越了。哦等等,還有第一種類型的泄露問題你沒有涉及到。
強引用週期
最後,你將尋找在Flickr圖片搜索app中的強引用環。正如以前提到的,當兩個對象互相持有對方的強引用時會出現強引用環。你能夠用另外一種方式使用Allocations工具來檢測這一環。
注意:爲保證你能跟上這篇教程的這一部分,你必須在一個真機上來分析你的app。不幸的是在寫該教程時,當在模擬器上運行app並啓用Allocations工具時會出現一個bug:大多數在工程中使用到的類沒法出如今Instruments中。
關閉Instruments,返回Xcode,確保你的app的構建目標選中爲真機設備。再一次選中Product\Profile,而後選擇Allocations模板。
這一次,你再也不使用分配分析,取而代之的是,你要看存在於內存中的不一樣類型對象的數量。你應該已經看過數量龐大的對象填充於詳情面板--數量太多以致於看不過來。
爲了篩選本身感興趣的對象,在Allocations Summary列表上方的文本框中輸入Instruments做爲篩選詞,這樣就只會顯示類型名中帶有Instruments關鍵詞的對象。由於咱們的示例工程名稱爲InstrumentsTutorial,Allocations列表將僅僅顯示這個工程中定義的那部分類型的對象。這樣就簡化了些工做。
這裏有兩列值得一提:#Persistent和#Transient,Persistent這一列記錄了存在於內存中的每一類型的對象的數量。Transient這一列記錄了曾經存在可是如今已經被銷燬了的對象的數量。Persistent對象(持久對象)正在使用內存,而Transient對象(臨時對象)已經將它們佔用的內存釋放了。
你應該能看到有一個持久對象實例:ViewController,那就對了,由於這就是你當前看到的界面。除此以外,還有AppDelegate,還有一個Flickr API客戶端的實例。
回到app中,執行一次搜索並點進詳情界面,注意到有大量新的對象出如今Instruments中:解析搜索結果時建立的FlickrPhotos、還有SearchResultsViewController、還有ImageCache,ViewController實例依然是持久對象,由於它被它的導航控制器持有,這樣很好。
如今按下返回按鈕,SearchResultsViewController被從導航棧中彈出
,因此它應該被銷燬。可是Allocations統計中#Presistent這一列依然顯示着數量爲1,爲何依然存在呢?
試着進行另外兩次搜索並每次都經過back按鈕返回,如今一共有3個SearchResultsViewControllers?!這些視圖控制器依然存在於內存中的事實說明有其餘對象持有它們的強引用,看起來你有一個強引用週期。
此時你的主要線索是,不僅SearchResultsViewController存在,全部的SearchResultsCollectionViewCells也存在。看起來好像保留環是存在於這兩個類之間的。
很不幸,在編寫本教程時,Instruments對swift的輸出在一些狀況下並非怎麼頗有用,這裏Instruments只能給你一些關於問題出在哪裏的提示,並展現對象從哪裏分配的,接下來解決問題就是你的工做了。
讓咱們去代碼中一探究竟。把鼠標放到Category一欄的InstrumentsTutorial.SearchResultsCollectionViewCell上面,點擊右邊的小箭頭,接下來的視圖展現了運行app時SearchResultsCollectionViewCells的全部分配狀況。有很是多的實例--每個查詢結果對應一個。
經過點擊面板頂部第三個按鈕切換檢查器到Extended Detail檢查器,這一檢查器顯示的是當前選中分配的棧追蹤。和以前的棧追蹤同樣,黑色部分是你的代碼,雙擊最頂部的黑色的一行(以InstrumentsTutorial開頭),看一下cell在哪被分配。
Cell是在collectionView(cellForRowAtIndexPath:)的一開始被分配的。若是你瀏覽接下來幾行,你會看到這個(很不幸,Instruments沒有給你提示顯示):
1
2
3
|
cell.heartToggleHandler = { isStarred in
self.collectionView.reloadItemsAtIndexPaths([ indexPath ])
}
|
這是處理點擊一個集合視圖單元格上的愛心按鈕的閉包,這就是產生循環引用的問題的地方,但這很難發現,除非你以前遇到過這種狀況。
Cell閉包經過self引用SearchResultsViewController,從而產生了一個強引用。實際上swift強制你在閉包中使用self(然而在指代當前對象的屬性和方法時你一般能夠省略它),這有助於加深你對正在捕獲self這一事實的認識。經過集合視圖,SearchResultsViewController也對這些cell持有強引用。
爲了打破強引用環,你能夠定義一個捕獲列表(capture list)做爲閉包定義的一部分,捕獲列表能夠用來聲明實例,這些實例被閉包捕獲時或者是weak,或者是unowned:
weak:當捕獲的引用在之後可能會變成nil時使用,若是引用的對象被釋放,引用變量自動變成nil。所以,這些變量都是可選類型。
Unowned:當被引用的對象和閉包擁有相同的生命週期而且會被同時釋放時使用,一個unowned變量永遠不多是nil。
要解決這個強引用環問題,再次點擊Open in Xcode按鈕,而後在SearchResultsViewController.swift的heartToggleHandler中添加捕獲列表:
1
2
3
4
5
|
cell.heartToggleHandler = { [weak self] isStarred in
if let strongSelf = self {
strongSelf.collectionView.reloadItemsAtIndexPaths([ indexPath ])
}
}
|
把self聲明爲weak說明SearchResultsViewController可能被釋放,即便集合視圖的cell持有它的一個引用。由於如今它們之間的引用僅僅是弱引用。而且SearchResultsViewController的釋放也會引發集合視圖的釋放,接着,cell釋放。
在Xcode中,再次使用commond+I在Instrument中編譯並運行app。
和以前作的同樣,在Instruments中,再次使用Allocations工具觀察app(記住要篩選結果,只顯示屬於咱們的示例工程部分的類)。執行一次搜索,導航到結果中,而後再次返回。能夠看到此次當你導航返回時SearchResultsViewController和它的cell都被釋放了。它們如今是臨時對象,而不是持久對象。循環打破!再一次跨越!:]
何去何從?
從這裏下載工程的最終優化版本,全都多虧了Instruments。
既然你已經掌握了這些知識,去分析本身的代碼而後看一下有什麼有趣的事情發生吧。同時,試着將分析應用做爲你日常開發工做流中的一個環節。
你應該常常經過Instruments來運行你的代碼,並在發佈以前對你的app進行一次完全的清理,以確保你已經儘量多地找到了內存管理問題和性能問題。
如今去作一些優秀而且高效的app吧!:]