不管您是在許多iOS應用程序上工做,仍是仍在開始使用第一個應用程序:您無疑會想出新功能,而且想知道您能夠作些什麼來使您的應用程序更加出色面試
除了經過添加功能改進您的應用程序以外,全部優秀的應用程序開發人員都應該作的一件事就是......檢測他們的代碼編程
本教程將向您展現如何使用Xcode附帶的名爲Instruments的工具的最重要功能。它容許您檢查代碼中的性能問題,內存問題,循環引用和其餘問題。swift
在本教程中,您將學習:api
注意:本教程假設您熟悉Swift和iOS編程。若是您是iOS編程的徹底初學者,您可能但願查看本網站上的其餘一些教程。本教程使用故事板,所以請確保您熟悉該概念;一個好的起點是本網站上的教程。緩存
搞定?準備好潛入迷人的Instuments世界! :]安全
對於本教程,您將不會從頭開始建立應用程序;相反,已經爲您提供了一個示例項目。您的任務是經過應用程序並使用Instruments做爲指南進行改進 - 與您優化本身的應用程序很是類似!bash
下載入門項目而後解壓縮並在Xcode中打開它。微信
此示例應用程序使用FlickrAPI搜索圖像。要使用API,您須要一個API密鑰。對於演示項目,您能夠在Flickr的網站上生成示例密鑰。進入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=6593783 efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f,API key就是:6593783efea8e7f6dfc6b70bc03d2afb。app
將其粘貼到FlickrAPI.swift的頂部,替換現有的API密鑰。
請注意,此示例API密鑰天天都會更改,所以您偶爾必須從新生成新密鑰。只要密鑰再也不有效,應用程序就會提醒您。
構建並運行應用程序,執行搜索,單擊結果,您將看到以下內容:
本教程的其他部分將向您展現如何查找和修復應用程序中仍存在的問題。您將看到Instruments如何使調試問題變得更加容易! :]
您將看到的第一個工具是Time Profiler。在測量的時間間隔內,Instruments將中止程序的執行並在每一個運行的線程上執行堆棧跟蹤。能夠將其視爲單擊Xcode調試器中的暫停按鈕。
如下是Time Profiler的預覽:
此屏幕顯示Call Tree。CallTree顯示在應用程序中的各類方法中執行所花費的時間。每行都是程序執行路徑所遵循的不一樣方法。在每種方法中花費的時間能夠根據每種方法中分析器中止的次數來肯定。
例如,若是以1毫秒的間隔完成100個樣本,而且發現特定方法位於10個樣本中的堆棧頂部,那麼您能夠推斷出總執行時間的大約10% - 10毫秒 - 花費了在那種方法中。這是一個至關粗略的近似,但它又不是不能用!
注意:一般,您應始終在實際設備上而不是模擬器上分析您的應用。 iOS模擬器具備Mac背後的全部功能,而設備將具備移動硬件的全部限制。您的應用程序彷佛在模擬器中運行得很好,可是一旦它在真實設備上運行,您可能會發現性能問題。 此外,Xcode 9測試版和使用Instruments的模擬器存在一些問題。
因此沒有任何進一步的麻煩,是時候去使用Time Profiler了!
從Xcode的菜單欄中,選擇Product \ Profile,或按⌘I。這將構建應用程序並啓動Instrument。您將看到一個以下所示的選擇窗口:
選擇Time Profiler,而後單擊「選擇」。這將打開一個新的Instruments文檔。單擊左上角的紅色記錄按鈕開始錄製並啓動應用程序。可能會要求您輸入密碼以受權儀器分析其餘過程 - 不要擔憂,這裏提供是安全的! :]
在窗口中,您能夠看到計時的時間,以及在屏幕中心的圖形上方從左向右移動的小箭頭。這代表該應用正在運行。
如今,開始使用該應用程序。搜索一些圖像,並深刻查看一個或多個搜索結果。您可能已經注意到,進入搜索結果的速度很是慢,滾動瀏覽搜索結果列表也很是煩人 - 這是一個很是笨重的應用程序!
嗯,你很幸運,由於你即將開始修復它!可是,您首先要快速瞭解您在Instruments中所看到的內容。
首先,確保工具欄右側的視圖選擇器同時選擇了兩個選項,以下所示:
單擊「Profile」一詞上此區域頂部的欄,而後選擇「Sample」。在這裏,您能夠查看每一個樣本。單擊幾個樣本,您將看到捕獲的堆棧跟蹤顯示在右側的「擴展詳細信息」檢查器中。完成後切換回Profile。 5. 這是檢查面板。有兩個檢查項:擴展詳細信息和運行信息。您很快就會了解有關這些選項的更多信息。
執行圖像搜索,並深刻查看結果。我我的喜歡搜索「狗」,但選擇你想要的任何東西 - 你多是那些愛貓人士之一! :]
如今,在列表中向上和向下滾動幾回,以便在Time Profiler中得到大量數據。您應該注意到屏幕中間的數字正在變化而且圖形填滿;這告訴您正在使用CPU週期。
沒有桌面視圖能夠運送,直到它像黃油同樣滾動!
爲了幫助查明問題,您須要設置一些選項。單擊「中止」按鈕,而後在詳細信息面板下單擊「Call Tree」按鈕。在出現的彈出窗口中,選擇「按線程分隔」,「反轉調用樹」和「隱藏系統庫」。它看起來像這樣:
掃描結果以肯定「Weight」列中哪些行具備最高百分比。請注意,具備主線程的行佔用了至關大比例的CPU週期。經過單擊文本左側的小箭頭展開此行,而後向下鑽取,直到您看到本身的方法之一(標有「人物」符號)。雖然某些值可能略有不一樣,但條目的順序應與下表相似:
要了解有關該方法中發生的更多信息,請雙擊表中的行。這樣作會顯示如下視圖:
那頗有意思,不是嗎! applyTonalFilter()是一個在擴展中添加到UIImage的方法,而且,在應用圖像過濾器以後,花費了大量時間來調用建立CGImage輸出的方法。
沒有太多能夠作的事情來加快速度:建立圖像是一個很是密集的過程,而且須要花費很長時間。讓咱們試着退後一步,看看調用applyTonalFilter()的位置。單擊代碼視圖頂部的痕跡導航路徑中的Root以返回上一個屏幕:
在這種狀況下,此行引用結果集合視圖(_:cellForItemAt :)。雙擊該行以查看項目中的關聯代碼。
如今您能夠看到問題所在。看看第74行;應用色調過濾器的方法須要很長時間才能執行,而且它直接從collectionView(_:cellForItemAt :)調用,每當它請求過濾後的圖像時,它將阻塞主線程(以及整個UI)。
要解決這個問題,您須要執行兩個步驟:首先,使用DispatchQueue.global().async將圖像過濾卸載到後臺線程上;而後在每一個圖像生成後對其進行緩存。初學者項目中包含一個簡單的小型圖像緩存類(引人注目的名稱爲ImageCache),它只是將圖像存儲在內存中並使用給定的密鑰檢索它們。
您如今能夠切換到Xcode並手動查找您在Instruments中查看的源文件,可是在您的眼前,有一個方便的Open in Xcode按鈕。在代碼上方的面板中找到它並單擊它:
如今,在collectionView(_:cellForItemAt:)中,使用下面的代碼代替loadThumbnail(for:completion:)的調用
ImageCache.shared.loadThumbnail(for: flickrPhoto) { result in
switch result {
case .success(let image):
if cell.flickrPhoto == flickrPhoto {
if flickrPhoto.isFavourite {
cell.imageView.image = image
} else {
if let cachedImage = ImageCache.shared.image(forKey: "\(flickrPhoto.id)-filtered") {
cell.imageView.image = cachedImage
}
else {
DispatchQueue.global().async {
if let filteredImage = image.applyTonalFilter() {
ImageCache.shared.set(filteredImage, forKey: "\(flickrPhoto.id)-filtered")
DispatchQueue.main.async {
cell.imageView.image = filteredImage
}
}
}
}
}
}
case .failure(let error):
print("Error: \(error)")
}
}
複製代碼
此代碼的第一部分與之前相同,而且涉及從Web加載Flickr照片的縮略圖圖像。若是照片被收藏,則單元格按原樣顯示縮略圖。可是,若是照片不受歡迎,則應用色調濾鏡。
這是您更改內容的地方:首先,檢查圖像緩存中是否存在此照片的已過濾圖像。若是是,那很好;您在圖像視圖中顯示該圖像。若是沒有,則調度該調用以將音調濾波器應用於後臺隊列。這將容許UI在過濾圖像時保持響應。應用過濾器後,將圖像保存在緩存中,並更新主隊列上的圖像視圖。
這是過濾後的圖像照片,但仍然有原始的Flickr縮略圖須要處理。打開Cache.swift並找到loadThumbnail(for:completion :)。將其替換爲如下內容:
func loadThumbnail(for photo: FlickrPhoto, completion: @escaping FlickrAPI.FetchImageCompletion) {
if let image = ImageCache.shared.image(forKey: photo.id) {
completion(Result.success(image))
}
else {
FlickrAPI.loadImage(for: photo, withSize: "m") { result in
if case .success(let image) = result {
ImageCache.shared.set(image, forKey: photo.id)
}
completion(result)
}
}
}
複製代碼
這與處理過濾圖像的方式很是類似。若是緩存中已存在圖像,則使用緩存的圖像直接調用完成閉包。不然,您從Flickr加載圖像並將其存儲在緩存中。
經過導航到Product \ Profile(或⌘I - 從新運行Instruments中的應用程序 - 請記住,這些快捷方式將爲您節省一些時間)。
請注意,此次Xcode不會詢問您使用哪一種儀器。這是由於您仍然爲此應用程序打開了一個窗口,而Instruments假定您但願使用相同的選項再次運行。
再執行一些搜索,注意此次UI不是那麼笨重!圖像過濾器如今異步應用,圖像在後臺緩存,所以一旦只須要過濾一次。您將在調用樹中看到許多dispatch_worker_threads - 這些正在處理應用圖像過濾器的繁重工做。
看起來很棒!是時候發貨了嗎?還沒! :]
那你接下來要追查什麼錯誤? :]
項目中隱藏着一些您可能不知道的東西。你可能據說過內存泄漏。但你可能不知道的是,實際上有兩種泄漏:
本教程中涉及的下一個工具是Allocations工具。這將爲您提供有關正在建立的全部對象以及支持它們的內存的詳細信息。它還顯示您保留每一個對象的計數。
要從新開始使用新儀器配置文件,請退出Instruments應用程序,不要擔憂保存此特定運行。如今按⌘I,從列表中選擇Allocations儀器,而後單擊Choose。
您將要執行的是「生成分析」。爲此,請單擊名爲Mark Generation的按鈕。您能夠在詳細信息面板底部找到按鈕:
通過幾回搜索後,儀器將以下所示:
此時,你應該開始懷疑。請注意您鑽取的每一個搜索的藍色圖表是如何上升的。嗯,那固然很差。但等等,內存警告怎麼樣?你瞭解那些,對嗎?內存警告是iOS告訴應用程序內存部門事情變得緊張的方式,你須要清除一些內存。
這種增加可能不只僅是由於你的應用程序;它多是UIKit深處持有內存的東西。在指向任何一個以前,先給系統框架和你的應用程序一個清除內存的機會。
經過選擇儀器菜單欄中的儀器\模擬內存警告或模擬器菜單欄中的硬件\模擬內存警告來模擬內存警告。您會注意到內存使用量略有降低,或者根本沒有降低。固然不會回到它應該的位置。因此在某個地方仍然存在無限的內存增加。
在每次迭代鑽取到搜索以後標記生成的緣由是您能夠看到在每個generation之間分配了哪些內存。看看細節面板,你會看到好幾個generation。
在每一個generation中,您將看到全部已分配的對象,而且在生成標記時仍然駐留。以後的generation將僅包含自上一generation標記後的對象。
看看增加列,你會發現某處確實存在增加。打開其中一代,你會看到:
簡單。單擊「增加」標題按大小排序,確保最重的對象位於頂部。在每一代的頂部附近,您會注意到一行標記爲ImageIO_jpeg_Data,這聽起來像您應用中處理的內容。單擊ImageIO_jpeg_Data左側的箭頭以顯示與此項目關聯的內存地址。選擇第一個內存地址以在右側面板的「擴展詳細信息」檢查器中顯示關聯的堆棧跟蹤:
看看這個方法,你會看到第81行調用set(_:forKey :)。請記住,這個方法會緩存一個圖像,以防之後在應用程序中再次使用它。啊!那確定聽起來多是個問題! :]
再次單擊「在Xcode中打開」按鈕以跳回Xcode。打開Cache.swift並看一下set(_:forKey :)的實現:
func set(_ image: UIImage, forKey key: String) {
images[key] = image
}
複製代碼
這會將圖像添加到字典中,該字典鍵入Flickr照片的照片ID。可是若是你查看代碼,你會發現圖像永遠不會從該字典中清除掉!
這就是你的無限內存增加來自:一切都在運行,但應用程序永遠不會從緩存中刪除東西 - 它只會添加它們!
要解決此問題,您須要作的就是讓ImageCache監聽UIApplication觸發的內存警告通知。當ImageCache收到此消息時,它必須是一個好公民並清除其緩存。
要使ImageCache監聽通知,請打開Cache.swift並將如下初始化程序和解除初始化程序添加到該類:
init() {
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationDidReceiveMemoryWarning, object: nil, queue: .main) { [weak self] notification in
self?.images.removeAll(keepingCapacity: false)
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
複製代碼
這會註冊UIApplicationDidReceiveMemoryWarningNotification的觀察者來執行上面的閉包,從而清除圖像。
代碼須要作的就是刪除緩存中的全部對象。這將確保再也不有任何東西保留在圖像上,它們將被解除分配。
要測試此修復程序,請再次啓動儀器(從Xcode使用⌘I)並重復以前執行的步驟。不要忘記最後模擬內存警告。
注意:確保從Xcode啓動,觸發構建,而不是僅僅點擊Instruments中的紅色按鈕,以確保您使用的是最新代碼。您可能還但願在分析以前首先構建並運行,由於有時Xcode彷佛不會將模擬器中應用程序的構建更新爲最新版本(若是您只是Profile)。
此次生成分析應以下所示:
之因此還有一些增加的緣由,其實是因爲系統庫,你能夠作的並很少。彷佛系統庫沒有釋放全部內存,這多是設計上的,也多是一個bug。您在應用程序中所能作的就是釋放盡量多的內存,而您已經完成了! :]
作得好!修補了另一個問題!如今必須是出貨的時候了!哦,等等 - 你尚未解決第一種泄漏問題。
如前所述,當兩個對象彼此保持強引用時會發生強引用循環,所以永遠不能釋放內存。您可使用Allocations儀器以不一樣的方式檢測這些循環。
關閉儀器並返回Xcode。再次選擇Product \ Profile,而後選擇Allocations模板。
您應該看到有一個ViewController的持久實例 - 這是有道理的,由於那是您當前正在查看的屏幕。還有應用程序AppDelegate的一個實例。
回到應用程序!執行搜索並深刻查看結果。請注意,如今儀器中出現了一堆額外的對象:SearchResultsViewController和ImageCache等。 ViewController實例仍然是持久的,由於它的導航控制器須要它。不要緊。
如今點按應用中的後退按鈕。 SearchResultsViewController現已從導航堆棧彈出,所以應該取消分配。但它仍然在分配總結中顯示#引用數爲1!它爲何還在那裏?
嘗試執行另外兩次搜索,而後在每次搜索後點擊後退按鈕。如今有3個SearchResultsViewControllers ?!這些視圖控制器在內存中閒置的事實意味着某些內容正在強烈引用它們。看起來你有一個嚴重的循環引用!
值得慶幸的是,Xcode 8中引入的Visual Memory Debugger是一個簡潔的工具,能夠幫助您進一步診斷內存泄漏和循環引用。 Visual Memory Debugger不是Xcode儀器套件的一部分,但它仍然是一個很是有用的工具,值得在本教程中包含。來自Allocations儀器和Visual Memory Debugger的交叉引用看法是一種強大的技術,可使您的調試工做流程更加有效。
退出Allocations儀器並退出儀器套件。
在啓動Visual Memory Debugger以前,在Xcode方案編輯器中啓用Malloc Stack日誌記錄,以下所示:單擊窗口頂部的Instruments Tutorial方案(中止按鈕旁邊),而後選擇Edit Scheme。在出現的彈出窗口中,單擊「運行」部分,而後切換到「診斷」選項卡。選中顯示Malloc Stack的框,而後選擇僅限實時分配選項,而後單擊關閉。
直接從Xcode啓動應用程序。像之前同樣,執行至少3次搜索以累積一些數據。
如今激活Visual Memory Debugger,以下所示:
Visual Memory Debugger暫停您的應用程序並顯示內存中對象的可視化表示以及它們之間的引用。
如上面的屏幕截圖所示,Visual Memory Debugger顯示如下信息:
請注意Debug導航器中的某些行如何在它們旁邊加上括號括起來的數字。括號中的數字表示該特定類型的實例在內存中的數量。在上面的屏幕截圖中,您能夠看到,通過少許搜索後,Visual Memory Debugger會確認您在Allocations工具中看到的結果,即從20到(若是您滾動到搜索結果的末尾)60個SearchResultsCollectionViewCell實例每一個SearchResultsViewController實例都保留在內存中。
使用行左側的箭頭展開類型並在內存中顯示每一個SearchResultsViewController實例。單擊單個實例將在主窗口中顯示該實例及其對它的任何引用。
在主窗口中選擇SearchResultsCollectionViewCell的實例,以顯示有關檢查器窗格的更多信息。
在回溯中,您能夠看到單元實例已在collectionView(_:cellForItemAt :)中初始化。當您將鼠標懸停在回溯中的此行上時,會出現一個小箭頭。單擊箭頭將轉到Xcode代碼編輯器中的此方法。 真棒!
在collectionView(_:cellForItemAt :)中,找到每一個單元格的heartToggleHandler變量的設置位置。您將看到如下代碼行:
cell.heartToggleHandler = { isStarred in
self.collectionView.reloadItems(at: [indexPath])
}
複製代碼
當點擊集合視圖單元格中的心形按鈕時,此閉包處理。這是強引用循環所在,但除非你以前遇到過,不然很難發現。可是因爲Visual Memory Debugger,您能夠跟蹤到這段代碼的全部路徑!
閉包Cell使用self引用了SearchResultsViewController,它建立了一個強引用。閉包持有了Self。 Swift實際上強迫你在閉包中明確使用self這個詞(而你一般能夠在引用當前對象的方法和屬性時刪除它)。這有助於您更加了解持有它的事實。 SearchResultsViewController還經過其集合視圖對單元格進行了強引用。
要打破強引用循環,能夠將捕獲列表定義爲閉包定義的一部分。捕獲列表可用於將閉包捕獲的實例聲明爲弱引用或無主引用:
要修復此強引用週期,請將捕獲列表添加到heartToggleHandler,以下所示:
cell.heartToggleHandler = { [weak self] isStarred in
self?.collectionView.reloadItems(at: [indexPath])
}
複製代碼
將self聲明爲Weak表示即便集合視圖單元格對其進行引用也能夠釋放SearchResultsViewController,由於它們如今只是弱引用。釋放SearchResultsViewController將取消分配其集合視圖,進而取消分配單元格。
在Xcode中,再次使用⌘+ I在Instruments中構建和運行應用程序。
使用Allocations儀器再次在Instruments中查看應用程序(請記住過濾結果以僅顯示做爲初始項目一部分的類)。執行搜索,導航到結果,而後再返回。您應該看到,當您向後導航時,SearchResultsViewController及其單元格如今已被釋放。它們顯示瞬態實例,但沒有持久實例。
循環被打破了,提交它吧!!
這是項目的最終優化版本的下載連接,這徹底歸功於Instruments。
既然您已經掌握了本教程中的知識,那就去測試本身的代碼,看看有什麼有趣的東西出現了!此外,嘗試使儀器成爲您一般的開發工做流程的一部分。
您應該常常經過Instruments運行代碼,並在發佈以前對應用程序進行全面掃描,以確保儘量多地捕獲內存管理和性能問題。
如今去製做一些很是棒且高效的應用! :]
PS:
最近加了一些iOS開發相關的QQ羣和微信羣,可是感受都比較水,裏面對於技術的討論比較少,因此本身建了一個iOS開發進階討論羣,歡迎對技術有熱情的同窗掃碼加入,加入之後你能夠獲得:
1.技術方案的討論,會有在大廠工做的高級開發工程師儘量抽出時間給你們解答問題
2.每週按期會寫一些文章,而且轉發到羣裏,你們一塊兒討論,也鼓勵加入的同窗積極得寫技術文章,提高本身的技術
3.若是有想進大廠的同窗,裏面的高級開發工程師也能夠給你們內推,而且針對性得給出一些面試建議