Xcode Instruments調試swift入門教程

不管您是在許多iOS應用程序上工做,仍是仍在開始使用第一個應用程序:您無疑會想出新功能,而且想知道您能夠作些什麼來使您的應用程序更加出色面試

除了經過添加功能改進您的應用程序以外,全部優秀的應用程序開發人員都應該作的一件事就是......檢測他們的代碼編程

本教程將向您展現如何使用Xcode附帶的名爲Instruments的工具的最重要功能。它容許您檢查代碼中的性能問題,內存問題,循環引用和其餘問題。swift

在本教程中,您將學習:api

  • 如何使用Time Profiler工具肯定代碼中的熱點,以便提升代碼的效率,以及
  • 如何使用Allocations工具和Visual Memory Debugger檢測和修復代碼中強引用週期等內存管理問題。

注意:本教程假設您熟悉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密鑰天天都會更改,所以您偶爾必須從新生成新密鑰。只要密鑰再也不有效,應用程序就會提醒您。

構建並運行應用程序,執行搜索,單擊結果,您將看到以下內容:

瀏覽應用程序並查看基本功能。您可能會想到,一旦UI看起來很棒,應用程序就能夠提交商店了。可是,您將看到使用Instruments能夠添加到您的應用程序的價值。

本教程的其他部分將向您展現如何查找和修復應用程序中仍存在的問題。您將看到Instruments如何使調試問題變得更加容易! :]

Time Profiler

您將看到的第一個工具是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。您將看到一個以下所示的選擇窗口:

這些都是Instrument附帶的不一樣模板。

選擇Time Profiler,而後單擊「選擇」。這將打開一個新的Instruments文檔。單擊左上角的紅色記錄按鈕開始錄製並啓動應用程序。可能會要求您輸入密碼以受權儀器分析其餘過程 - 不要擔憂,這裏提供是安全的! :]

在窗口中,您能夠看到計時的時間,以及在屏幕中心的圖形上方從左向右移動的小箭頭。這代表該應用正在運行。

如今,開始使用該應用程序。搜索一些圖像,並深刻查看一個或多個搜索結果。您可能已經注意到,進入搜索結果的速度很是慢,滾動瀏覽搜索結果列表也很是煩人 - 這是一個很是笨重的應用程序!

嗯,你很幸運,由於你即將開始修復它!可是,您首先要快速瞭解您在Instruments中所看到的內容。

首先,確保工具欄右側的視圖選擇器同時選擇了兩個選項,以下所示:

這將確保全部面板都是打開的。如今研究下面的截圖以及它下面每一個部分的解釋:

  1. 這些是錄音控件。點擊紅色的「記錄」按鈕將中止或者啓動當前正在分析的應用程序(它在記錄和中止圖標之間切換)。暫停按鈕徹底符合您的預期,並暫停當前應用程序的執行。
  2. 這是運行計時器。計時器計算正在運行的應用程序運行的時間長度以及運行的次數。單擊中止按鈕,而後從新啓動應用程序,您將看到顯示屏如今顯示Run 2 of 2。
  3. 這被稱爲軌道。對於您選擇的Time Profiler模板,只有一個儀器,所以只有一個軌道。您將在本教程後面的內容中詳細瞭解該圖的具體細節。
  4. 這是細節面板。它顯示了您正在使用的特定儀器的主要信息。在這種狀況下,它顯示的是「最熱門」的方法 - 也就是那些耗盡了大部分CPU時間的方法。

單擊「Profile」一詞上此區域頂部的欄,而後選擇「Sample」。在這裏,您能夠查看每一個樣本。單擊幾個樣本,您將看到捕獲的堆棧跟蹤顯示在右側的「擴展詳細信息」檢查器中。完成後切換回Profile。 5. 這是檢查面板。有兩個檢查項:擴展詳細信息和運行信息。您很快就會了解有關這些選項的更多信息。

深刻

執行圖像搜索,並深刻查看結果。我我的喜歡搜索「狗」,但選擇你想要的任何東西 - 你多是那些愛貓人士之一! :]

如今,在列表中向上和向下滾動幾回,以便在Time Profiler中得到大量數據。您應該注意到屏幕中間的數字正在變化而且圖形填滿;這告訴您正在使用CPU週期。

沒有桌面視圖能夠運送,直到它像黃油同樣滾動!

爲了幫助查明問題,您須要設置一些選項。單擊「中止」按鈕,而後在詳細信息面板下單擊「Call Tree」按鈕。在出現的彈出窗口中,選擇「按線程分隔」,「反轉調用樹」和「隱藏系統庫」。它看起來像這樣:

如下是每一個選項對左側表格中顯示的數據的做用:

  • Seprate by State:此選項按應用程序的生命週期狀態對結果進行分組,這是檢查應用程序正在執行的工做量和時間的有用方法。
  • Seprate by Thread:每一個線程都應該單獨考慮。這使您能夠了解哪些線程負責最大量的CPU使用。
  • Invert Call Tree:使用此選項,堆棧跟蹤將被視爲從最遠到最近。
  • Hide System Libraries:選擇此選項後,僅顯示您本身的應用程序中的符號。選擇此選項一般頗有用,由於一般只關心CPU在您本身的代碼中花費時間的位置 - 您沒法對系統庫使用的CPU量作多少工做!
  • Flatten Recursion:此選項將遞歸函數(自稱爲自身的函數)視爲每一個堆棧跟蹤中的一個條目,而不是多個。
  • Top Function:啓用此功能會使Instruments將在函數中花費的總時間視爲該函數內直接時間的總和,以及該函數調用的函數所花費的時間。所以,若是函數A調用B,則A的時間被報告爲在A PLUS中花費的時間在B中花費的時間。這可能很是有用,由於它容許您在每次降低到調用堆棧時選擇最大的時間數字,歸零在你最耗時的方法。

掃描結果以肯定「Weight」列中哪些行具備最高百分比。請注意,具備主線程的行佔用了至關大比例的CPU週期。經過單擊文本左側的小箭頭展開此行,而後向下鑽取,直到您看到本身的方法之一(標有「人物」符號)。雖然某些值可能略有不一樣,但條目的順序應與下表相似:

嗯,這固然看起來不太好。絕大部分時間都用在將「色調」濾鏡應用於縮略圖照片的方法中。這不該該對你形成太大的衝擊,由於表格加載和滾動是UI中最笨重的部分,並且當表格單元格不斷更新時。

要了解有關該方法中發生的更多信息,請雙擊表中的行。這樣作會顯示如下視圖:

那頗有意思,不是嗎! applyTonalFilter()是一個在擴展中添加到UIImage的方法,而且,在應用圖像過濾器以後,花費了大量時間來調用建立CGImage輸出的方法。

沒有太多能夠作的事情來加快速度:建立圖像是一個很是密集的過程,而且須要花費很長時間。讓咱們試着退後一步,看看調用applyTonalFilter()的位置。單擊代碼視圖頂部的痕跡導航路徑中的Root以返回上一個屏幕:

如今單擊表頂部applyTonalFilter行左側的小箭頭。這將顯示applyTonalFilter的調用者。您可能還須要展開下一行;在分析Swift時,調用樹中有時會出現重複的行,前綴爲@objc。您對以「person」符號爲前綴的第一行感興趣,該符號表示它屬於您應用的目標:

在這種狀況下,此行引用結果集合視圖(_:cellForItemAt :)。雙擊該行以查看項目中的關聯代碼。

如今您能夠看到問題所在。看看第74行;應用色調過濾器的方法須要很長時間才能執行,而且它直接從collectionView(_:cellForItemAt :)調用,每當它請求過濾後的圖像時,它將阻塞主線程(以及整個UI)。

卸載工做

要解決這個問題,您須要執行兩個步驟:首先,使用DispatchQueue.global().async將圖像過濾卸載到後臺線程上;而後在每一個圖像生成後對其進行緩存。初學者項目中包含一個簡單的小型圖像緩存類(引人注目的名稱爲ImageCache),它只是將圖像存儲在內存中並使用給定的密鑰檢索它們。

您如今能夠切換到Xcode並手動查找您在Instruments中查看的源文件,可是在您的眼前,有一個方便的Open in Xcode按鈕。在代碼上方的面板中找到它並單擊它:

我去! Xcode在恰當的位置打開。Boom!

如今,在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, Allocations, Allocations

那你接下來要追查什麼錯誤? :]

項目中隱藏着一些您可能不知道的東西。你可能據說過內存泄漏。但你可能不知道的是,實際上有兩種泄漏:

  1. 真正的內存泄漏是一個對象再也不被任何東西引用但仍被分配的東西 - 這意味着永遠不能重用內存。 即便使用Swift和ARC幫助管理內存,最多見的內存泄漏類型是循環持有或循環強引用。這是當兩個對象彼此持有強引用時,每一個對象使另外一個對象不被釋放。這意味着他們的記憶永遠不會被釋放。
  2. 無限的內存增加是繼續分配內存而且永遠不會被釋放的機會。若是這種狀況持續下去,那麼在某些時候系統的內存將被填滿,你的手上會有很大的內存問題。在iOS上,這意味着該應用程序將被系統殺死。

本教程中涉及的下一個工具是Allocations工具。這將爲您提供有關正在建立的全部對象以及支持它們的內存的詳細信息。它還顯示您保留每一個對象的計數。

要從新開始使用新儀器配置文件,請退出Instruments應用程序,不要擔憂保存此特定運行。如今按⌘I,從列表中選擇Allocations儀器,而後單擊Choose。

如今,您應該看到Allocations工具。它應該看起來很熟悉,由於它看起來很像Time Profiler。
單擊左上角的「錄製」按鈕以運行該應用程序。此次你會注意到兩個軌道。出於本教程的目的,您將只關注名爲All Heap和Anonymous VM的那個。
在應用程序上運行Allocations工具後,在應用程序中進行五次不一樣的搜索,但不會深刻查看結果。確保搜索有一些結果。如今讓應用程序等待幾秒鐘。
您應該已經注意到All Heap和Anonymous VM軌道中的圖形一直在上升。這告訴你正在分配內存。正是這個功能將引導您找到無限的內存增加。

您將要執行的是「生成分析」。爲此,請單擊名爲Mark Generation的按鈕。您能夠在詳細信息面板底部找到按鈕:

單擊它,您將看到軌道中出現一個紅色標記,以下所示:
生成分析的目的是屢次執行操做,並查看內存是否以無限制的方式增加。深刻搜索,等待幾秒鐘以加載圖像,而後返回主頁面。而後再次標記一代。對不一樣的搜索重複執行此操做。

通過幾回搜索後,儀器將以下所示:

此時,你應該開始懷疑。請注意您鑽取的每一個搜索的藍色圖表是如何上升的。嗯,那固然很差。但等等,內存警告怎麼樣?你瞭解那些,對嗎?內存警告是iOS告訴應用程序內存部門事情變得緊張的方式,你須要清除一些內存。

這種增加可能不只僅是由於你的應用程序;它多是UIKit深處持有內存的東西。在指向任何一個以前,先給系統框架和你的應用程序一個清除內存的機會。

經過選擇儀器菜單欄中的儀器\模擬內存警告或模擬器菜單欄中的硬件\模擬內存警告來模擬內存警告。您會注意到內存使用量略有降低,或者根本沒有降低。固然不會回到它應該的位置。因此在某個地方仍然存在無限的內存增加。

Instruments: 談論個人Generation

在每次迭代鑽取到搜索以後標記生成的緣由是您能夠看到在每個generation之間分配了哪些內存。看看細節面板,你會看到好幾個generation。

在每一個generation中,您將看到全部已分配的對象,而且在生成標記時仍然駐留。以後的generation將僅包含自上一generation標記後的對象。

看看增加列,你會發現某處確實存在增加。打開其中一代,你會看到:

哇,那有不少對象!你該從哪裏開始呢?

簡單。單擊「增加」標題按大小排序,確保最重的對象位於頂部。在每一代的頂部附近,您會注意到一行標記爲ImageIO_jpeg_Data,這聽起來像您應用中處理的內容。單擊ImageIO_jpeg_Data左側的箭頭以顯示與此項目關聯的內存地址。選擇第一個內存地址以在右側面板的「擴展詳細信息」檢查器中顯示關聯的堆棧跟蹤:

此堆棧跟蹤顯示建立此特定對象的時間點。灰色的堆棧跟蹤部分位於系統庫中;黑色部分在您的應用程序代碼中。嗯,看起來很熟悉:一些黑色條目顯示你的老朋友collectionView(_:cellForItemAt :)。雙擊任何這些條目,Instruments將在其上下文中顯示代碼。

看看這個方法,你會看到第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模板。

這一次,您將不會使用生成分析。相反,您將查看內存中不一樣類型的對象數量。單擊「錄製」按鈕開始此運行。您應該已經看到大量的對象填滿了細節面板 - 太多沒法看清楚!要縮小到感興趣的對象,請在左下角的字段中鍵入「Instruments」做爲過濾器。
儀器中值得注意的兩個欄目是#Persistent和#Transient。 Persistent列保留內存中當前存在的每種類型的對象數的計數。 「Transient」列顯示已存在但已取消分配的對象數。持久對象正在耗盡內存,瞬態對象已釋放內存。

您應該看到有一個ViewController的持久實例 - 這是有道理的,由於那是您當前正在查看的屏幕。還有應用程序AppDelegate的一個實例。

回到應用程序!執行搜索並深刻查看結果。請注意,如今儀器中出現了一堆額外的對象:SearchResultsViewController和ImageCache等。 ViewController實例仍然是持久的,由於它的導航控制器須要它。不要緊。

如今點按應用中的後退按鈕。 SearchResultsViewController現已從導航堆棧彈出,所以應該取消分配。但它仍然在分配總結中顯示#引用數爲1!它爲何還在那裏?

嘗試執行另外兩次搜索,而後在每次搜索後點擊後退按鈕。如今有3個SearchResultsViewControllers ?!這些視圖控制器在內存中閒置的事實意味着某些內容正在強烈引用它們。看起來你有一個嚴重的循環引用!

您在這種狀況下的主要線索是,不只SearchResultsViewController持久存在,並且全部SearchResultsCollectionViewCells也是如此。循環引用可能在這兩個類之間。

值得慶幸的是,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,以下所示:

  1. 切換到Debug導航器。
  2. 單擊此圖標,而後從彈出窗口中選擇「View Memory Graph Hierarchy」。
  3. 單擊SearchResultsCollectionViewCell的條目。
  4. 您能夠單擊圖形上的任何對象以查看檢查器窗格中的詳細信息。
  5. 您能夠查看此區域的詳細信息。切換到內存檢查器。

Visual Memory Debugger暫停您的應用程序並顯示內存中對象的可視化表示以及它們之間的引用。

如上面的屏幕截圖所示,Visual Memory Debugger顯示如下信息:

  • 堆內容(調試導航器面板):顯示應用程序暫停時在內存中分配的全部類型和實例的列表。單擊某個類型會展開該行,以顯示內存中類型的單獨實例。
  • 內存圖(主面板):主窗口顯示內存中對象的直觀表示。對象之間的箭頭表示它們之間的引用(強關係和弱關係)。
  • 內存檢查器(「實用程序」面板):這包括類名稱和層次結構等詳細信息,以及引用是強仍是弱。

請注意Debug導航器中的某些行如何在它們旁邊加上括號括起來的數字。括號中的數字表示該特定類型的實例在內存中的數量。在上面的屏幕截圖中,您能夠看到,通過少許搜索後,Visual Memory Debugger會確認您在Allocations工具中看到的結果,即從20到(若是您滾動到搜索結果的末尾)60個SearchResultsCollectionViewCell實例每一個SearchResultsViewController實例都保留在內存中。

使用行左側的箭頭展開類型並在內存中顯示每一個SearchResultsViewController實例。單擊單個實例將在主窗口中顯示該實例及其對它的任何引用。

注意指向SearchResultsViewController實例的箭頭。看起來有一些Swift閉包上下文實例引用了同一個視圖控制器實例。看起來有點懷疑,不是嗎?讓咱們仔細看看。選擇其中一個箭頭以在「實用工具」窗格中顯示有關其中一個閉包實例與SearchResultsViewController之間的引用的更多信息。
在Memory Inspector中,您能夠看到Swift閉包上下文和SearchResultsViewController之間的引用是強引用。若是選擇SearchResultsCollectionViewCell和Swift閉包上下文之間的引用,您將看到它也標記爲強引用。您還能夠看到閉包的名稱是「heartToggleHandler」.A-ha!這是在SearchResultsCollectionViewCell類中聲明的!

在主窗口中選擇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還經過其集合視圖對單元格進行了強引用。

要打破強引用循環,能夠將捕獲列表定義爲閉包定義的一部分。捕獲列表可用於將閉包捕獲的實例聲明爲弱引用或無主引用:

  • 當捕獲的參考可能在未來變爲零時,應該使用Weak。若是它們引用的對象被釋放,則引用變爲零。所以,它們是可選類型。
  • 當閉包及其引用的對象將始終具備相同的生命週期而且將同時取消分配時,應使用Unowned。無主引用永遠不會置爲nil。

要修復此強引用週期,請將捕獲列表添加到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.若是有想進大廠的同窗,裏面的高級開發工程師也能夠給你們內推,而且針對性得給出一些面試建議

相關文章
相關標籤/搜索