如何使用Instruments診斷App(Swift版):起步

jopen 9年前發布 | 21K 次閱讀 Swift Apple Swift開發

如何使用Instruments診斷App(Swift版):起步
本文由Mr_cyz(博客)翻譯自raywenderlich,歡迎參與我們的翻譯活動
原文:Instruments Tutorial with Swift: Getting Started



更新記錄:該教程由 James Frost更新至iOS8,swift語言。 原版本由我們隊伍中的一員Matt Galloway編寫。

無論你寫過許多iOS應用,還是剛剛開始你的第一個應用,毫無疑問,你都會想出一些新點子,或者想去弄明白你該怎么做,來讓你的app變得更好。

除去添加新特性來優化你的應用,有一件事是所有好的開發者都回去做的,那就是診斷他們的代碼。

該教程將向你展示怎么樣去使用Xcode提供的工具"Instrument"中最重要的一些功能。幫助你檢查自己代碼中的性能問題、內存管理問題、循環引用問題以及其他種種。

在本篇教程中,你將學到:

  • 怎樣使用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不可用了,你的應用將會提醒你。

編譯并運行應用,執行一次查詢,然后點擊一個結果,你將會看到類似下面的界面。

如何使用Instruments診斷App(Swift版):起步

瀏覽一下這個應用,弄清楚基本的功能,你可能會想,一旦UI看起來不錯后,這個應用就準備好上傳了。然而,接下來你將看到使用Instruments工具后將為你的app帶來多少好處。

本教程剩下的內容將會向你展示怎么樣找到并改正存在于你的應用中的問題。你將看到Instruments工具怎么樣使debug程序的工作變得易如反掌。

如何使用Instruments診斷App(Swift版):起步時間分析儀

首先你將使用的工具是Time Profiler。在每個測量時間間隔內,該工具將暫停程序執行,在每個線程上進行一次棧追蹤(stack trace),可以想象成點了Xcode調試工具中的暫停鍵。

這里有一張Time Profiler的預覽圖。

如何使用Instruments診斷App(Swift版):起步

這個界面展示的是調用樹(call tree)。調用樹展示的是一個app中執行不同的方法花費的時間,每一行都是程序執行路徑中的一個不同的方法,每個方法花費的時間可以由分析工具在其中暫停的次數來決定。

例如,如果有100件事情要做,每件花費1毫秒,在棧頂的方法做了其中10件,那么你可以推斷出,大約在總執行時間中的10%--10毫秒--花費在了這個方法中。這是相當粗糙的估計,但確實有效!

注意:通常來說,你應該總是在真機上分析你的app,而不是在模擬器上。iOS模擬器有你的Mac提供的性能支撐,但是真機作為硬件移動設備,資源是有限的。所以你的app可能在模擬器上運行得很好,但是一旦它運行到真機上,你可能就會發現有性能問題。

那么立刻開始分析吧。

從Xcode的菜單欄中,選擇product/profile,或者按下commond+I,這時會編譯程序,加載Instruments工具,然后會出現一個選擇框,類似于下面的圖片:

如何使用Instruments診斷App(Swift版):起步

Instruments提供了不同的模板。

選擇Time Profiler工具,然后點擊Choose,這時會出現一個新的工具文件。點擊左上角的紅色記錄按鈕,開始記錄并加載你的app,你可能需要輸入密碼來為Instruments分析其他進程授權--不用擔心,這很安全。

在Instruments窗口中,可以看到一個計時器,還有一個小箭頭在屏幕中央的圖表上從左向右移動。這表明app正在運行。

現在開始使用這款app,搜索圖片,然后點擊幾個查詢結果進入詳情界面,你可能會發現進入一個詳情界面非常慢,另外滑動查詢結果的列表也是慢得難以置信--這是一款笨重的app。

然而,你是幸運的,因為接下來你就會修正這一問題。不過在這之前你要先快速瀏覽一下當前展示的這個Instruments的界面。

首先,確保右手邊工具欄上的視圖選擇器的每一個選項都被選中,如下:

如何使用Instruments診斷App(Swift版):起步

這樣就確保所有的面板都被打開。現在看一下下面的截圖和每一部分的說明。

如何使用Instruments診斷App(Swift版):起步

1、這里控制記錄過程,點擊紅色的"記錄"按鈕可以停止或開始當前正在分析的app(在記錄和停止按鈕之間切換),暫停鍵,如你所想,暫停當前正在運行的app。

2、這里是執行計時器(run timer),計時器記錄著正在分析的app執行了多長時間、執行了多少次。如果你使用記錄控制按鈕來停止你的app,然后重啟,這將創建一個新的運行記錄,同時會顯示"Run 2 of 2"。

3、這里被稱作路徑(track),就你選擇的Time Profiler工具而言,因為只有一個工具,所以這里只有一條路徑,關于這里顯示的圖標的詳情,一會你就會在接下來的教程中了解更多。

4、這里是詳情面板,展示的是你正在使用的工具的主要信息。就現在而言,這里展示的是最"笨重(hottest)"的方法--換句話說,占用CPU時間最長的方法。點擊上方的bar會看到Call Tree(左手邊的那個)并選中Sample List,然后你會看到數據的不同視圖。視圖展示了每一個示例。點擊其中幾個,你會在Extended Detail inspector中看到被捕獲的堆棧跟蹤。

5、這里是檢查器(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選項,你的界面應該看起來是這樣的:

如何使用Instruments診斷App(Swift版):起步

下面解釋了每一個選項對左側列表中數據的顯示起了什么作用:

  • 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++的函數。

盡管一些值可能會有輕微的不同,不過如果你選中了上面提到的幾個選項后,列表中展示的入口的順序應該是類似于下圖的:

如何使用Instruments診斷App(Swift版):起步

額,這看起來不怎么好,大量的時間被花在設置縮略圖的"色調"濾鏡('tonal'filter)的方法上了。這應該不會太讓你驚訝,因為列表的加載與滾動是UI中最笨重的部分,而這里正式列表單元格被持續加載的地方。

為了解到更多關于這個方法做了什么的信息,雙擊列表中的這一行,這樣將把你帶到下面的視圖中:

如何使用Instruments診斷App(Swift版):起步

這很有趣,不是嗎?applyTonalFilter()是一個UIImage擴展中的一個方法,幾乎100%的時間被花費在這個方法中的應用圖片濾鏡后創建CGImage輸出這一地方了。

我們沒辦法為這一過程加速,創建一張圖片是個費時的過程。讓我們回退一步,看看applyTonalFilter()是從哪里調用的。點擊代碼界面的頂部欄中的Call Tree,回到上一界面。

如何使用Instruments診斷App(Swift版):起步

然后點擊列表頂部applyTonalFilter左側的小箭頭,這樣就展開了Call Tree,展示出applyTonalFilter的調用者。你可能需要再展開到下一行。當你分析的是swift代碼時,有時在Call Tree中會出現重復的一行,以@objc為前綴,此時你只需要關心第一行,以你的app的target名稱為前綴(本例為InstrumentsTutorial)。

如何使用Instruments診斷App(Swift版):起步

這里,該行代指collection view的cellForItemAtIndexPath方法的結果,雙擊該行可以看到工程中相關的代碼。

現在你知道問題出在哪了。應用色調濾鏡的方法占用了較長的時間,而該方法又直接從cellForItemAtIndexPath中調用,這樣每當該方法要求一個被濾鏡渲染的圖片時都會會阻塞主線程(整個UI)。

卸下重任

要解決這一問題,可以分兩步來:首先使用dispatch_async將創建濾鏡的方法放到后臺線程,接著在每一張圖片被創建后都緩存起來。我們的工程中有一個簡單的圖片緩存類(有一個易記的名字:ImageCache),簡單地將圖片保存到內存中,然后通過給定的鍵來獲取它們)。

現在可以切換到Xcode上,手動找到當前你正在Instruments中看的源文件,不過現在在你的眼前,右側就有一個快捷按鈕Open in Xcode,在面板的代碼部分的上面找到它并點擊:

如何使用Instruments診斷App(Swift版):起步

這樣,Xcode就定位到正確的位置了。

接下來,在collectionView(_:cellForItemAtIndexPath:)方法中,把調用loadThumbnail()方法替換為下面的代碼:

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(_:),將其替換為:

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就可以在主窗口中顯示內存的使用圖表。

如何使用Instruments診斷App(Swift版):起步

這些圖表可以幫你大體上了解你的app的表現,不過你需要更強大的功能。點擊Profile in Instruments按鈕,然后可以把這部分轉換到Instruments中。Allocations工具會自動打開。

如何使用Instruments診斷App(Swift版):起步

這次你需要注意兩個追蹤,第一個叫做分配(Allocations),第二個是泄露(Leaks),分配追蹤將在下文詳細討論,通常泄露追蹤在Objective-C中更有用,所以本篇教程不會涉及。

那么接下來你將去查找哪個bug呢?

有些事被隱藏在工程中,你可能不知道它的存在。你可能聽說過內存泄露,但不知道其實有兩種泄露:

1、"真正的內存泄露(True memory leaks)"是指一個對象不再被引用但卻沒有被釋放--這說明內存永遠不能被復用,即使有swift和ARC幫助管理內存,最常見的內存泄露問題是保留環,或稱為強引用環。當兩個對象互相持有對方的強引用時,每個對象保證另一個不會被釋放,這樣它們的內存將永遠不能被釋放!

2、"無限內存增長(Unbounded memory growth)"是指內存持續被分配而沒有機會被釋放。如果這一現象永遠持續下去,某一點上系統資源將被占滿,這樣你就親手創建了一個大的內存問題。在iOS上意味著你的app將被系統殺死。

Allocations工具運行在app上時,進行五次不同的搜索,但不要點進詳細界面,確保每次搜索都有一些結果,現在讓app靜止等待幾秒鐘。

你應該能注意到Allocations追蹤中的圖表一直在增長,這說明內存正在被分配,這一特點將指導你找到無限內存增長問題。

接下來你要執行"分配分析(generation analysis)",要做到這一點,點擊Mark Generation按鈕,你可以在Display Setting檢查器的頂部找到這一按鈕。

如何使用Instruments診斷App(Swift版):起步

按下它,你將會發現一個紅旗出現在追蹤中,如下:

如何使用Instruments診斷App(Swift版):起步

分配分析的目的是多次執行一個事件,查看內存是否以無限的形式增長,點擊進入搜索的詳情界面,等待幾秒鐘的圖片加載,然后返回主頁,再一次mark generation,對于不同的搜索重復幾次這樣的操作。

在進入幾次詳情界面以后,Instruments將看起來如下圖所示:

如何使用Instruments診斷App(Swift版):起步

這時你應該會有所起疑,可以注意到每次搜索并進入詳情界面后藍色的圖表都在增長,這樣肯定不好。不過等一下,內存警告呢?你應該知道的,內存警告是iOS告訴app內存緊缺的一種方式,并通知你你需要清理一些內存。

有可能這種增長不僅僅是你的app造成的,它可能是UIKit內部使用內存的結果。所以在指定具體哪一個出現問題之前,給系統框架和你的app一個機會來清理自己的內存。

可以在Instruments的菜單欄中選擇Instrument\Simulate Memory Warning來模擬一次內存警告,或者從模擬器的菜單欄中選擇Hardware\Simulate Memory Warning。你會注意到內存使用圖下陷了一點,也可能根本沒有。很顯然使用圖沒有回到應該的位置上,因此你的程序的某處依然有無限內存增長的問題。

每次點入詳情界面后都做一次標記的原因是,你可以看到在每個標記段之間哪些內存被分配了。看一眼詳情面板,你會發現有大量的內存分配。

漫談分配

在每一個generation段中,你可以看到所有自標記以來被分配了內存空間,并且一直存活的對象。隨后的每個generation段中只包含自上一個標記之后的符合上述描述的對象。

看一眼Growth欄,你就會發現肯定在某處存在著增長問題,展開其中一個generation,你會看到如下圖界面:

如何使用Instruments診斷App(Swift版):起步

哇,有好多的對象,我們從哪開始呢?

很不幸,在這一界面上swift比Objective-C雜亂得多,因為這里充滿了你并不需要了解的內部數據結構。你可以通過切換Allocation Type至All Heap Allocations來方便地清除掉它們。當然也可以點擊頂部的Growth頭,讓對象按照大小排序。

最頂部的對象是ImageIO_jpeg_Data,并且這肯定是你的app創造的對象。點擊ImageIO_jpeg_Data左側的箭頭展開詳情列表,選中一行,然后打開Extended Detail檢查器(或者按下commond+3)。

如何使用Instruments診斷App(Swift版):起步

這里顯示的是當指定對象被創建時的棧追蹤,灰色部分的屬于系統框架,黑色部分是你的app中的。要了解這一追蹤的更多信息,雙擊黑色部分倒數第二行,這是唯一以InstrumentsTutorial開頭的一行,代表它是來自swift代碼的。雙擊它會把你帶到相關方法的代碼界面--你的老朋友collectionView(_:cellForItemAtIndexPath:)。

Instruments非常有用,但是這里它不能幫你更多了,現在你必須親自瀏覽一遍代碼來了解這里到底發生了什么。

看一遍代碼,你會發現它調用了setImage(_:forKey:)方法,正如你在Time Profiler中看到的,這個方法緩存圖像以便之后在app中復用。啊哈,聽起來就像一個問題。

再次點擊Open in Xcode跳入Xcode界面,打開ImageUtilities.swift,看一下setImage(_:forKey:)的實現:

func setImage(image: UIImage, forKey key: String) {
  images[key] = image
}

這里以Flickr的圖片ID作為鍵,將圖片保存到字典中。但是如果你整體瀏覽一遍代碼,你會發現圖片永遠不會從字典中被清除。

這就是你的無限內存增長的來源:所有事情都按照設定來工作,但是app永遠不會清除緩存--它只是不斷地往里增加。

要解決這一問題,你需要做的是讓ImageCache監聽從UIApplication發來的內存警告的通知。當它收到通知后就清除掉它的緩存。

要讓ImageCache監聽通知,在該類中添加init和deinit方法:

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編譯更新到最新代碼上。

這一次的分配分析應該看起來是這樣的:

如何使用Instruments診斷App(Swift版):起步

可以發現在內存警告之后內存的使用下跌了。總體上依然有很多內存增長,但是不像之前那樣多了。

現在依然有很多內存增長是由系統庫造成的,并且你也沒法對其做一些改進。這些系統庫并沒有釋放它們的全部內存,這有可能是刻意設計的,也有可能是一個bug。你能對你的app做的就是盡可能多地釋放內存,而這一點你已經做到了! :]

非常好!又解決了一個問題!是時候進行新的跨越了。哦等等,還有第一種類型的泄露問題你沒有涉及到。

強引用周期

最后,你將尋找在Flickr圖片搜索app中的強引用環。正如之前提到的,當兩個對象互相持有對方的強引用時會出現強引用環。你可以用另一種方式使用Allocations工具來檢測這一環。

注意:為保證你能跟上這篇教程的這一部分,你必須在一個真機上來分析你的app。不幸的是在寫該教程時,當在模擬器上運行app并啟用Allocations工具時會出現一個bug:大多數在工程中使用到的類無法出現在Instruments中。

關閉Instruments,返回Xcode,確保你的app的構建目標選中為真機設備。再一次選中Product\Profile,然后選擇Allocations模板。

如何使用Instruments診斷App(Swift版):起步

這一次,你不再使用分配分析,取而代之的是,你要看存在于內存中的不同類型對象的數量。你應該已經看過數量龐大的對象填充于詳情面板--數量太多以至于看不過來。

為了篩選自己感興趣的對象,在Allocations Summary列表上方的文本框中輸入Instruments作為篩選詞,這樣就只會顯示類型名中帶有Instruments關鍵詞的對象。因為我們的示例工程名稱為InstrumentsTutorial,Allocations列表將僅僅顯示這個工程中定義的那部分類型的對象。這樣就簡化了些工作。

如何使用Instruments診斷App(Swift版):起步

這里有兩列值得一提:#Persistent#Transient,Persistent這一列記錄了存在于內存中的每一類型的對象的數量。Transient這一列記錄了曾經存在但是現在已經被銷毀了的對象的數量。Persistent對象(持久對象)正在使用內存,而Transient對象(臨時對象)已經將它們占用的內存釋放了。

你應該能看到有一個持久對象實例:ViewController,那就對了,因為這就是你當前看到的界面。除此之外,還有AppDelegate,還有一個Flickr API客戶端的實例。

回到app中,執行一次搜索并點進詳情界面,注意到有大量新的對象出現在Instruments中:解析搜索結果時創建的FlickrPhotos、還有SearchResultsViewController、還有ImageCache,ViewController實例依然是持久對象,因為它被它的導航控制器持有,這樣很好。

現在按下返回按鈕,SearchResultsViewController被從導航棧中彈出

,所以它應該被銷毀。但是Allocations統計中#Presistent這一列依然顯示著數量為1,為什么依然存在呢?

試著進行另外兩次搜索并每次都通過back按鈕返回,現在一共有3個SearchResultsViewControllers?!這些視圖控制器依然存在于內存中的事實說明有其他對象持有它們的強引用,看起來你有一個強引用周期。

如何使用Instruments診斷App(Swift版):起步

此時你的主要線索是,不只SearchResultsViewController存在,所有的SearchResultsCollectionViewCells也存在。看起來好像保留環是存在于這兩個類之間的。

很不幸,在編寫本教程時,Instruments對swift的輸出在一些情況下并不是怎么很有用,這里Instruments只能給你一些關于問題出在哪里的提示,并展示對象從哪里分配的,接下來解決問題就是你的工作了。

讓我們去代碼中一探究竟。把鼠標放到Category一欄的InstrumentsTutorial.SearchResultsCollectionViewCell上面,點擊右邊的小箭頭,接下來的視圖展示了運行app時SearchResultsCollectionViewCells的所有分配情況。有非常多的實例--每一個查詢結果對應一個。

如何使用Instruments診斷App(Swift版):起步

通過點擊面板頂部第三個按鈕切換檢查器到Extended Detail檢查器,這一檢查器顯示的是當前選中分配的棧追蹤。和之前的棧追蹤一樣,黑色部分是你的代碼,雙擊最頂部的黑色的一行(以InstrumentsTutorial開頭),看一下cell在哪被分配。

Cell是在collectionView(cellForRowAtIndexPath:)的一開始被分配的。如果你瀏覽接下來幾行,你會看到這個(很不幸,Instruments沒有給你提示顯示):

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中添加捕獲列表:

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吧!:]

來自:http://www.cocoachina.com/ios/20150623/12237.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!