电竞比分网-中国电竞赛事及体育赛事平台

分享

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

 爽行天下丶 2015-06-24

Screen-Shot-2015-03-20-at-23.54.11-303x320.png本文由Mr_cyz(博客)翻譯自raywenderlich,歡迎參與我們的翻譯活動。
原文:Instruments Tutorial with Swift: Getting Started



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

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

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

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

在本篇教程中,你將學(xué)到:

  • 怎樣使用Time Profiler工具來定位你的代碼中的"高消耗點(hot-spot)",從而讓你的代碼更加有效率。

  • 怎樣使用Allocations工具來檢測和改正代碼中的內(nèi)存管理問題,例如循環(huán)強引用。

注意:本教程假定你已經(jīng)上手了iOS開發(fā)和swift語言。如果你是iOS開發(fā)的初學(xué)者,你可能更適合去看一下本網(wǎng)站上的其他教程。本篇教程還使用了storyboard,所以確保你熟悉相關(guān)概念。本網(wǎng)站上的這篇教程是一個很好的起點。
(編輯注:如果你想全面了解Instruments,請參看:Instruments 用戶指南【中文完整翻譯版】)

一切就緒?準備好進入instrument的迷人的世界中吧。

起步

在本篇教程中,你無需從頭開始創(chuàng)建一個完整的應(yīng)用,我們已經(jīng)為你提供了一個示例程序,你的任務(wù)是瀏覽這個應(yīng)用,然后使用instrument作為你的助手來改善這個應(yīng)用--類似于你優(yōu)化自己的應(yīng)用的過程。

從這里下載starter project,解壓后使用Xcode打開。

該示例程序使用Flickr提供的API來搜索圖片。你需要一個API key來使用這個API。對樣例程序而言,你可以去Flickr的網(wǎng)站上創(chuàng)建一個樣例key,然后就可以通過網(wǎng)站http://www./services/api/explore/?method=flickr.photos.search 來搜索圖片,并使用時把API key拷貝到上述url的最后面,格式為"&api_key=",接下來的參數(shù)同樣加到&后面。

例如,如果URL是http://api./services/rest/?method=flickr.photos.search&api_key=6593783efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f  ,那么API key就是6593783efea8e7f6dfc6b70bc03d2afb。

把這個key粘貼到FlickrSearcher.swift文件頂部,取代原有的key。

需要注意的是,該key每隔一天左右都會改變,所以你可能碰巧需要去重新生成一個key。如果key不可用了,你的應(yīng)用將會提醒你。

編譯并運行應(yīng)用,執(zhí)行一次查詢,然后點擊一個結(jié)果,你將會看到類似下面的界面。

iOS-Simulator-Screen-Shot-21-Mar-2015-00.01.35-281x500.png

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

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

TimeProfiler.png時間分析儀

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

這里有一張Time Profiler的預(yù)覽圖。

Screen-Shot-2015-03-21-at-00.07.10-480x269.png

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

例如,如果有100件事情要做,每件花費1毫秒,在棧頂?shù)姆椒ㄗ隽似渲?0件,那么你可以推斷出,大約在總執(zhí)行時間中的10%--10毫秒--花費在了這個方法中。這是相當粗糙的估計,但確實有效!

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

那么立刻開始分析吧。

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

Screen-Shot-2015-03-21-at-00.12.32-480x270.png

Instruments提供了不同的模板。

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

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

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

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

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

Screen-Shot-2015-03-21-at-00.14.53.png

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

Pasted_Image_21_03_2015_00_17-451x320.png

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

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

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

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

5、這里是檢查器(inspector)面板,一共有三個檢查器:record setting(記錄設(shè)置),display setting(展示設(shè)置),還有extends detail(擴展詳情)。一會你將了解更多關(guān)于這里面的一些選項。

現(xiàn)在開始診斷這笨重的UI!:]

更進一步

搜索一次圖片,然后點擊結(jié)果進入詳情界面,我個人喜歡搜索"狗",不過選一個你喜歡的就好--你可能是想搜索貓的一員:]

現(xiàn)在連續(xù)上下滾動列表數(shù)次,這樣你就在Time Profile工具中得到足夠的數(shù)據(jù)了,可以發(fā)現(xiàn)屏幕中央的數(shù)字在改變,圖表也開始被填充,這說明正在占用CPU循環(huán)。

你當然不希望任何UI如此笨重,那么table view就絕對不會被忽略,除非它滾動起來非常流暢。

要定位這里的問題,你需要設(shè)置一些選項。

在右手邊,選擇display setting(或者按下commond+2),在該選擇器中,在Call Tree欄下選中Separate by Thread, Invert Call Tree, Hide Missing Symbols Hide System Libraries選項,你的界面應(yīng)該看起來是這樣的:

21.png

下面解釋了每一個選項對左側(cè)列表中數(shù)據(jù)的顯示起了什么作用:

  • Separate by Thread:每個線程被單獨考慮。這能讓你知道哪一個線程占用CPU最多。

  • Invert Call Tree:選中該選項后,調(diào)用棧會自上至下顯示。這通常是你需要的,因為你想知道CPU花費時間的那個最深的方法。

  • Hide Missing Symbols:如果在你的app或者框架中找不到dSYM文件,那么你將只能在列表中看到二進制代碼中的十六進制地址值,而不是方法的名稱(符號)。選中該選項后,只有能被解析的符號可以被顯示出來,未被解析的十六進制數(shù)值會被隱藏,這有助于清理顯示的數(shù)據(jù)。

  • Hide System Libraries:選中該選項后,只有你自己app中出現(xiàn)的符號會被顯示出來。通常選中該選項是有用的,因為你只關(guān)心CPU在你自己的代碼中的哪一部分花費時間,你沒法對系統(tǒng)庫使用CPU做多少改變。

  • Flatten Recursion:該選項將每一個調(diào)用棧中的遞歸函數(shù)(調(diào)用它們自身的函數(shù))視作單一入口,而不是多入口。

  • Top Functions:選上這一選項讓Instruments將花費在一個函數(shù)中的總時間視作在該函數(shù)中直接花費的時間加上調(diào)用的其他函數(shù)花費的時間。所以如果函數(shù)A調(diào)用了函數(shù)B,那么函數(shù)A花費的總時間被記為A花費的時間加上B花費的時間。這一選項非常有用,因為它能讓你在每次進入調(diào)用棧時找到花費最長的時間,瞄準你最耗時的方法。

如果你正在使用Objective-C寫的app,那么這里還有一個選項:Show Obj-C Only,選擇該選項后,只展示Objective-C方法,不展示其他任何C或C++的函數(shù)。目前你的app中沒有C或C++函數(shù),但是舉例來說,如果你正在看的是一款OpenGL應(yīng)用,那么可能會有一些C++的函數(shù)。

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

27.png

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

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

28.png

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

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

29.png

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

55.png

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

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

卸下重任

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

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

56.png

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

接下來,在collectionView(_:cellForItemAtIndexPath:)方法中,把調(diào)用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
            })
        }
        })
      }
    }
  }
}

這段代碼的第一部分和之前一樣,從網(wǎng)絡(luò)上加載Flickr的圖片,如果該圖片被渲染過,那么cell直接展示相應(yīng)的縮略圖,如果沒有被渲染過,就將色調(diào)濾鏡應(yīng)用到圖片上。

接下來就是改變的地方,首先代碼檢查圖片的濾鏡是否存在于圖片緩存中,如果是,那么直接交由image view展示,如果沒有,那么為圖片添加色調(diào)濾鏡的方法被分配到后臺隊列中執(zhí)行,當該濾鏡被渲染好以后,將渲染后的圖片保存到緩存中,在主線程中讓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)
    }
  }
}

這里與處理濾鏡圖片類似,如果一張圖片已經(jīng)存在于緩存中,那么直接用緩存的圖片來調(diào)用completion回調(diào),否則從Flickr上請求圖片并保存到緩存中。

通過Product/Profile(或者commond+I,記住,這些快捷鍵可以節(jié)省你大量時間)打開Instruments,重新運行app。

可以發(fā)現(xiàn)這一次你不需要選擇使用哪個工具,因為你的app仍然在一個窗口中打開著,Instruments假定你想以同樣的選項再次運行。

進行幾次搜索,可以發(fā)現(xiàn)這次UI不是那么慢了,現(xiàn)在圖片濾鏡是異步渲染,圖片也在后臺被緩存,所以它們只需要被渲染一次,可以在Call Tree中看到幾個dispatch_worker_threads,這里是處理繁重的加載圖片濾鏡的過程。

看起來不錯,是時候做一次跨越了:]

分配、分配、分配

本教程要介紹的下一個工具是Allocations工具,它可以給你關(guān)于所有被創(chuàng)建的對象和它們背后使用的內(nèi)存的詳細信息。它也能顯示出每個對象的引用計數(shù)。

要打開一個新的分析工具,首先退出Instruments工具。這次,編譯并運行app,在導(dǎo)航欄中點開Debug欄,然后點擊Memory就可以在主窗口中顯示內(nèi)存的使用圖表。

56.png

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

57.png

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

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

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

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

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

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

你應(yīng)該能注意到Allocations追蹤中的圖表一直在增長,這說明內(nèi)存正在被分配,這一特點將指導(dǎo)你找到無限內(nèi)存增長問題。

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

58.png

按下它,你將會發(fā)現(xiàn)一個紅旗出現(xiàn)在追蹤中,如下:

59.png

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

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

61.png

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

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

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

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

漫談分配

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

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

61.png

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

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

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

63.png

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

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

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

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

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

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

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

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

要讓ImageCache監(jiān)聽通知,在該類中添加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的觀察者來執(zhí)行上面的閉包,清除圖片緩存。

代碼需要做的就是移除緩存中的所有對象,這樣就確保這些圖像不再占有什么資源,它們將被釋放掉。

為了測試這一修改,再次啟動Instruments(在Xcode中按下快捷鍵commond+I),重復(fù)之前的步驟,別忘了最后模擬一次內(nèi)存警告。

注意:確保你是從Xcode中啟動并經(jīng)過編譯,而不是僅僅按下Instruments中的紅色按鈕,這樣才能確保你使用的是最新的代碼。你也可能需要在進行分析之前先編譯運行一次,因為有時如果你直接分析,那么Xcode似乎沒有將模擬器中的app編譯更新到最新代碼上。

這一次的分配分析應(yīng)該看起來是這樣的:

64.png

可以發(fā)現(xiàn)在內(nèi)存警告之后內(nèi)存的使用下跌了??傮w上依然有很多內(nèi)存增長,但是不像之前那樣多了。

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

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

強引用周期

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

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

關(guān)閉Instruments,返回Xcode,確保你的app的構(gòu)建目標選中為真機設(shè)備。再一次選中Product\Profile,然后選擇Allocations模板。

65.png

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

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

66.png

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

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

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

現(xiàn)在按下返回按鈕,SearchResultsViewController被從導(dǎo)航棧中彈出

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

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

67.png

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

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

讓我們?nèi)ゴa中一探究竟。把鼠標放到Category一欄的InstrumentsTutorial.SearchResultsCollectionViewCell上面,點擊右邊的小箭頭,接下來的視圖展示了運行app時SearchResultsCollectionViewCells的所有分配情況。有非常多的實例--每一個查詢結(jié)果對應(yīng)一個。

68.png

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

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

cell.heartToggleHandler = { isStarred in
  self.collectionView.reloadItemsAtIndexPaths([ indexPath ])
}

這是處理點擊一個集合視圖單元格上的愛心按鈕的閉包,這就是產(chǎn)生循環(huán)引用的問題的地方,但這很難發(fā)現(xiàn),除非你之前遇到過這種情況。

Cell閉包通過self引用SearchResultsViewController,從而產(chǎn)生了一個強引用。實際上swift強制你在閉包中使用self(然而在指代當前對象的屬性和方法時你通??梢允÷运?,這有助于加深你對正在捕獲self這一事實的認識。通過集合視圖,SearchResultsViewController也對這些cell持有強引用。

為了打破強引用環(huán),你可以定義一個捕獲列表(capture list)作為閉包定義的一部分,捕獲列表可以用來聲明實例,這些實例被閉包捕獲時或者是weak,或者是unowned:

  • weak:當捕獲的引用在以后可能會變成nil時使用,如果引用的對象被釋放,引用變量自動變成nil。因此,這些變量都是可選類型。

  • Unowned:當被引用的對象和閉包擁有相同的生命周期并且會被同時釋放時使用,一個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說明SearchResultsViewController可能被釋放,即使集合視圖的cell持有它的一個引用。因為現(xiàn)在它們之間的引用僅僅是弱引用。并且SearchResultsViewController的釋放也會引起集合視圖的釋放,接著,cell釋放。

在Xcode中,再次使用commond+I在Instrument中編譯并運行app。

和之前做的一樣,在Instruments中,再次使用Allocations工具觀察app(記住要篩選結(jié)果,只顯示屬于我們的示例工程部分的類)。執(zhí)行一次搜索,導(dǎo)航到結(jié)果中,然后再次返回??梢钥吹竭@次當你導(dǎo)航返回時SearchResultsViewController和它的cell都被釋放了。它們現(xiàn)在是臨時對象,而不是持久對象。循環(huán)打破!再一次跨越!:]

何去何從?

從這里下載工程的最終優(yōu)化版本,全都多虧了Instruments。

既然你已經(jīng)掌握了這些知識,去分析自己的代碼然后看一下有什么有趣的事情發(fā)生吧。同時,試著將分析應(yīng)用作為你平常開發(fā)工作流中的一個環(huán)節(jié)。

你應(yīng)該經(jīng)常通過Instruments來運行你的代碼,并在發(fā)布之前對你的app進行一次徹底的清理,以確保你已經(jīng)盡可能多地找到了內(nèi)存管理問題和性能問題。

現(xiàn)在去做一些優(yōu)秀并且高效的app吧!:]

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多