iOS 開發:用 Instruments 來檢驗你的app

編者注:或許不少人對 Instruments 應用不太瞭解,但可能不少老的 iOS 開發者都應該用過 Instruments 工具來檢測iOS應用內存泄漏狀況。特別是在iOS 5.0以前,即蘋果在iOS平臺上面還沒支持ARC的時候,寫iOS應用就相似C語言那樣,容易忘記釋放內存,而內存對移動設備而言是很是難得的。ios

入門

爲了節省你們的時間,提供一個演示的Demo給你們.代碼傳送門.
下載後解壓而後用xcode打開.
編譯運行APP後 而後在搜索框內輸入任意詞彙,點擊結果你會看到下面的結果
請輸入圖片描述git

正如你所見的,這個app很簡單.程序其實調用的是Flickr的API,經過app頂部的搜索框執行搜索後在下面的tableview顯示你搜索的搜索詞,搜索詞後面的括號內有搜索結果的個數,點擊此行進入一個略縮圖的結果列表頁面 如上圖. 點擊其中一行 進入圖像的大圖模式,在這個頁面你能夠根據須要旋轉圖像.
到目前爲止頁面看起來差很少了,你也許會想應該能夠直接提交appstore了吧.接下來這篇文章將會教你instruments工具來提升你app性能和穩定性.github

「時間探測器」

天下武功,惟快不破。不少公司都信奉這個教條.巴不得把app壓法週期壓縮到最低,這就致使了開發中隱藏了不少問題,有點經驗的工程師草率的優化下,更糟的狀況那些沒有經驗的工程師甚至不會對app進行任何優化.算法

某種程度上來講,你開發過程當中是能夠忽略性能優化的. 十年前,移動設備的硬件資源是很是有限的.甚至連浮點數都是被禁止的.由於浮點數能致使代碼變大計算的速度變慢.segmentfault

科技發展如此迅速的今天,硬件很大程度上能夠彌補軟件的短板.如今的移動設備3D硬件處理的效率甚至媲美於PC機了,可是你不能總依賴於硬件和處理器速度來掩飾你APP作的多垃圾吧.(若是安卓系統跑在Iphone上還可以像IOS同樣順滑嗎?,實際上是一個道理的)xcode

性能這個概念很抽線,因此咱們必須藉助數據化圖形化的輸出方式.你可能花一週的時間去優化一個有趣的算法,可是這算法只佔總執行時間的0.5%,無論你花多少精力去優化它,沒人會注意到.相反一個for循環花費了90%的時間,你稍微修改下就能提升10%的效率,就是這個簡單的修改能夠獲得你們很大的好感.由於.他們運行app時的第一感覺就是比以前快了不少.沒人會care你修改的是一個多牛逼的算法,仍是一個簡單的for循環.瀏覽器

這個說明了什麼? 與其花費時間在優化小細節上不如多點時間找到你改優化的地方.緩存

下面引出第一個工具 「時間事件查看器」(本身杜撰的名字英文—Time Profiler),———他能夠測量時間的間隔,中斷程序執行,跟蹤每一個線程的堆棧.你能夠想象下是xcode調試時按下暫停時的畫面
請輸入圖片描述性能優化

好比,100個樣本都在作1毫秒的間隔,而後在某個方法堆棧頂部有10個樣本,你能夠推算出大概的時間有10%個10毫秒花費在此方法中,這是一個近似值.網絡

廢話少說,時間是個檢測到的.

從xcode的菜單選擇Product-Profile,或者選擇⌘I,
請輸入圖片描述

程序會啓動Instruments,這時候你會看到一個選擇窗口
請輸入圖片描述

這是instruments全部測試儀器的面板,選擇 「timer profilter」 點擊「profile」回啓東模擬器和app,此時會要求你輸入一次密碼,以便instruments能有權限去截獲監聽此進程.
請輸入圖片描述

在工具窗口中,能夠看到時間計數,並留下了一個小箭頭移動到右側的圖形在屏幕的中央上方。這代表該應用程序正在運行。

如今開始運行app,搜索一些圖片,這時候你發現查找一個結果太慢了,並且搜索結果列表頁面滾動起來也是讓人沒法忍受的,
首先,確保工具欄中的視圖選擇有選擇的全部三個選項,以下所示:
請輸入圖片描述

這將確保全部的面板都打開。如今,研究下面的截圖和它下面的每一個部分的解釋:
請輸入圖片描述

  1. 錄控按鈕。中間的紅色按鈕將中止與啓動它被點擊時,應用程序目前正在分析。注意這其實是中止和啓動應用程序,而不是暫停它。

  2. 運行定時器和運行導航,定時器顯示APP已經運行了多長時間,箭頭之間是能夠移動的。若是中止,而後使用錄製按鈕從新啓動應用程序,這將開始一個新的運行。顯示屏便會顯示「run2 of 2」,你能夠回到第一次運行的數據,首先你中止當前運行,而後按下左箭頭回去。

  3. 運行軌道.

  4. 擴展面板,在時間探查儀器的狀況下,它是用來跟蹤顯示堆棧

  5. 詳細地面板。它顯示了你正在使用的儀器的主要信息,這是使用頻率最高的部門,能夠從它這裏看到cpu運行的時間

  6. 選項面板 稍後介紹

重頭戲來了.

深究

執行圖像搜索,並深究結果。我我的比較喜歡尋找「狗」,固然你也能夠選擇任意你想要的內容.好比貓啊美女啊什麼的.

如今上下滾動記下列表,讓時間探測器測量下數據,而後注意看下屏幕的變化和數值.這些數值反應了CPU週期.

可是你也許會發現下面的數值太多,看你的眼花繚亂. 下面打開左邊的調用樹 而後按着以下的配置
請輸入圖片描述

如下介紹下配置選項:

  • Separate by Thread: 每一個線程應該分開考慮。只有這樣你才能揪出那些大量佔用CPU的"重"線程
  • Invert Call Tree: 從上倒下跟蹤堆棧,這意味着你看到的表中的方法,將已從第0幀開始取樣,這一般你是想要的,只有這樣你才能看到CPU中話費時間最深的方法.也就是說FuncA{FunB{FunC}} 勾選此項後堆棧以C->B-A 把調用層級最深的C顯示在最外面
  • Hide Missing Symbols: 若是dSYM沒法找到你的app或者系統框架的話,那麼表中看不到方法名只能看到十六進制的數值,若是勾線此項能夠隱藏這些符號,便於簡化數據
  • Hide System Libraries: 勾選此項你會顯示你app的代碼,這是很是有用的. 由於一般你只關心cpu花在本身代碼上的時間不是系統上的
  • Show Obj-C Only: 只顯示oc代碼 ,若是你的程序是像OpenGl這樣的程序,不要勾選側向由於他有多是C++的
  • Flatten Recursion: 遞歸函數, 每一個堆棧跟蹤一個條目
  • Top Functions: 一個函數花費的時間直接在該函數中的總和,以及在函數調用該函數所花費的時間的總時間。所以,若是函數A調用B,那麼A的時間報告在A花費的時間加上B.花費的時間,這很是真有用,由於它可讓你每次下到調用堆棧時挑最大的時間數字,歸零在你最耗時的方法。

若是您已啓用上述選項,雖然有些值可能會略有不一樣,下面的結果的順序應該是相似下表:
請輸入圖片描述
經過上面你能看到大部分時間都花在更新表格照片了.

雙擊此行,而後將會看到以下
請輸入圖片描述

那麼這頗有趣,不是嗎!幾乎四分之三的時間花費在setPhoto:方法都花在創造照片的圖像數據!
如今能夠看到的是什麼問題,NSData’s dataWithContentsOfURL 方法並不會當即返回,由於要從網上去數據,每次調用都須要長達幾秒的時間返回,而此方法運行在主線程,可想而知會有什麼結果了.
其實爲了解決這個問題,類提供了一個ImageCache 的後臺異步下載的方法.

如今,您能夠切換到Xcode和手動找到該文件,但儀器有一個方便的「打開Xcode中」按鈕,就在你的眼前。找到它的面板只是上面的代碼並單擊它:
請輸入圖片描述

想以下修改

- (void)setPhoto:(FlickrPhoto *)photo {
    _photo = photo;

    self.textLabel.text = photo.title;f

//    NSData *imageData = [NSData dataWithContentsOfURL:_photo.thumbnailUrl]; 
//    self.imageView.image = [UIImage imageWithData:imageData];

    [[ImageCache sharedInstance] downloadImageAtURL:_photo.thumbnailUrl
                                  completionHandler:^(UIImage *image) {
                                      self.imageView.image = image;
                                      [self setNeedsLayout];
                                  }];
}

修改好厚,在儀器從新運行該應用程序Product—Profile(或⌘I-記住,這些快捷鍵真的會爲您節省一些時間)。
請注意,這個時候會再問一次你是否使用一塊兒。這是由於你還有一個窗口中打開這個程序,及儀器假定您要使用相同的選項再次運行。
執行一些更多的搜索,並注意此時用戶界面不是那麼卡頓了!這些圖像如今異步加載,並緩存在後臺,因此一旦他們已經被下載一次,他們沒必要再次下載。
看上去很不錯!是時候發佈了嗎? 固然還不夠

分配,分配,分配

接下來的儀器是分配工具。它能給出你全部建立和存儲它們的內存的詳細信息,它也顯示你保留了每一個對象的計數。

關閉儀器,回到Xcode和選擇Product->Profile。而後,從選擇器分配並單擊配置文件。以下圖:
請輸入圖片描述

程序再次打開 而後你會看到
請輸入圖片描述

這個時候你會發現兩個曲目。一個叫(分配)Allocations,以及一個被稱爲VM Tracker(虛擬機跟蹤)。該分配軌道將詳細在本教程中討論;虛擬機跟蹤也是很是有用的,但更復雜一點。
因此你的錯誤會追蹤下?
有隱藏的項目,你可能不知道有東西在那兒。你可能已經據說了內存泄漏。但你可能不知道的是,其實有兩種泄漏。
第一個是真正的內存泄漏,一個對象還沒有被釋放,可是再也不被引用的了。所以,存儲器不能被從新使用。
第二類泄漏是比較麻煩一些。這就是所謂的「無界內存增加」。這發生在內存繼續分配,並永遠不會有機會被釋放。
若是永遠這樣下去你的程序佔用的內存會無限大,當超過必定內存的話 會被系統的看門狗給kill掉.

創建一個場景,你能夠檢測出無限的內存增加。首先,在應用程序使10個不一樣的搜索(不要用已經存在的搜索)。確保搜索的一些結果!如今讓程序等待幾秒鐘。

你應該已經注意到,在分配的軌道圖不斷上升。這是告訴你的,內存被分配了。它的這一特徵,將引導你找到無限的內存增加。
你將要執行的是「heap shot analysis」。爲此,按這個按鈕叫「Mark Heap」。你會發現的詳細面板左側的按鈕
請輸入圖片描述

按下它,你會看到一個紅色的標誌出如今軌道上,像這樣:
請輸入圖片描述

heap shot分析的目的是執行一個動做屢次,看看若是內存是否無限增加。搜索一個內容,稍等幾秒加載圖像,而後返回主頁。而後再標記堆。反覆這樣作不一樣的搜索。
演戲幾個搜索後,儀器會看起來像這樣:
請輸入圖片描述

這時你應該會疑問。圖中的藍色是怎麼回事了,你繼續這樣操做10次這樣的搜索 藍色還不斷變高:
那確定是很差的。別急,有什麼關於內存的警告?你知道這些,對不對?內存警告是告訴一個應用程序,內存警告是ios處理app最好的方式尤爲是在內存愈來愈吃緊的時候,你須要清除一些內存。
內存一直增加其實也不必定是你的代碼除了問題,也有多是UIKit 系統框架自己致使的.

經過選擇HardwareSimulate內存警告在iOS模擬器的菜單欄模擬內存警告。你會發現,記憶體使用量出現小幅回落,但絕對不會回到它應該的。因此仍是有無限的內存增加發生的地方。
究其緣由,堆出手作鑽進搜索的每次迭代後,你能夠看到內存的分配每一個鏡頭之間。一塊兒來看看在詳細信息面板,你會看到一堆一堆的鏡頭。

在iOS模擬器的菜單欄中選擇hardwaresimulate內存警告模擬內存警告。你會發現內存使用會出現小幅回落,但確定不會回到它應該在的地方。
每一次的搜索後作你能夠看到內存已拍攝之間的分配。在詳細信息面板看一看,你會看到一好多堆鏡頭。

穩準狠

第一個堆鏡頭做爲參照,而後隨便打開一個堆鏡頭,你會看到以下:
請輸入圖片描述

靠,這是一個很大的對象!從哪裏開始看呢?
最好的方式是經過列表,你在你的應用程序直接使用的類。在這種狀況下,HTTPHeaderDict,CGRegion,CGPath,CFNumber,等等都是能夠忽略了。
可是,一個突出的是UIImage,這確定是在你程序使用的。點擊上的UIImage左側的箭頭顯示的完整列表。選擇一個,在擴展詳細信息面板:
請輸入圖片描述

圖中灰色的是系統庫,黑色部分是你應用的代碼,要得到此跟蹤更多的上下文,雙擊惟一的黑框ImageCache方法,這時候將掉轉到以下方法

- (void)downloadImageAtURL:(NSURL*)url completionHandler:(ImageCacheDownloadCompletionHandler)completion {
    UIImage *cachedImage = [self imageForKey:[url absoluteString]];
    if (cachedImage) {
        completion(cachedImage);
    } else {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            [self setImage:image forKey:[url absoluteString]];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(image);
            });
        });
    }
}

工具是很是有用的,你如今要努力經過本身的代碼思考發生了什麼.看看經過上面的方法,你會看到它調用一個名爲setImage方法:forKey:。這種方法在緩存以防它再次使用之後的應用程序的圖像。啊!那麼這確定聽起來像它多是一個問題!
一塊兒來看看該方法的實現:

- (void)setImage:(UIImage*)image forKey:(NSString*)key {
    [_cache setObject:image forKey:key];
}

從網絡上下載一個圖片添加字典中,你會注意到這些圖片歷來沒有從字典清楚過
,這就是內存爲何會一直增加,由於應用程序並不會從緩存裏刪除東西.它只會一直增長他們.
要解決此問題,你須要的是ImageCache收到UIApplication內存吃緊的警告時.清理緩存.

爲了使ImageCache能接收通知,修改ini​​t方法以下:

- (id)init {
    if ((self = [super init])) {
        _cache = [NSMutableDictionary new];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(memoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

註冊UIApplicationDidReceiveMemoryWarningNotification執行memoryWarning:方法。

- (void)memoryWarning:(NSNotification*)note {
    [_cache removeAllObjects];
}

memoryWarning刪除緩存中的全部對象。這將確保沒有持圖像。
爲了測試此修復程序,再次啓動儀器(從Xcode中有⌘I)和重複的步驟。不要忘了在模擬結束內存警告!

注意:請確保您從Xcode中退出,從新構建,而不是僅僅點擊儀器儀表上的紅色按鈕,以確保您使用的是最新的代碼。

這一次分析應該是這樣的:

這個時候,內存受到內存警告後急劇降低。但仍是有一些內存總體增加,但遠不及像之前那樣。
究其緣由仍是有必定的增加確實是因爲系統庫,並無太多能夠作的。看來,系統庫不釋放全部的內存,這多是由設計或多是一個錯誤。你能夠在你的應用程序作的是釋放盡量多的內存越好,你已經作到這一點!
幹得好!還有一個問題,修補了, - 仍然有泄漏,你尚未解決的第一種類型的問題。

內存泄露

內存泄漏的儀器。這是用來找到第一類泄漏前面提到的 - 當一個對象再也不被引用時出現的那種
檢測泄漏是能夠理解的一個很複雜的事情,但泄漏的工具記得,已分配的全部對象,並按期經過掃描每一個對象以肯定是否有任何不能從任何其餘對象訪問的。
關閉儀器,回到Xcode和選擇Product->Profile
請輸入圖片描述

回到你的應用程序!執行搜索,獲得結果。而後點選結果的預覽行打開全屏瀏覽器。按下旋轉按鈕在左上角,而後再按一次。
回到儀器,等待片刻。若是你已經正確地完成上述步驟後,你會發現泄漏已經出現了!你的工具窗口將看起來像這樣:
請輸入圖片描述

返回到模擬器,並按下旋轉幾回。而後返回到儀器和等會,獲得以下結果:
請輸入圖片描述

哪來的泄漏從哪裏來?擴展詳細信息面板
請輸入圖片描述

在擴展的詳細信息面板打開CGContext上名單。在列表中選擇CGContext上的元素之一,這代表致使要建立的對象,以下面的堆棧跟蹤:
請輸入圖片描述

再次,涉及到你的代碼中的幀顯示爲黑色。因爲只有一個,雙擊它,看看代碼的方法。
有問題的方法是rotateTapped: ,這是被調用時被竊聽旋轉按鈕的處理程序。這種方法旋轉原始圖像,並建立一個新的圖像,以下:

- (void)rotateTapped:(id)sender {
    UIImage *currentImage = _imageView.image;
    CGImageRef currentCGImage = currentImage.CGImage;

    CGSize originalSize = currentImage.size;
    CGSize rotatedSize = CGSizeMake(originalSize.height, originalSize.width);

    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 rotatedSize.width,
                                                 rotatedSize.height,
                                                 CGImageGetBitsPerComponent(currentCGImage),
                                                 CGImageGetBitsPerPixel(currentCGImage) * rotatedSize.width,
                                                 CGImageGetColorSpace(currentCGImage),
                                                 CGImageGetBitmapInfo(currentCGImage));

    CGContextTranslateCTM(context, rotatedSize.width, 0.0f);
    CGContextRotateCTM(context, M_PI_2);
    CGContextDrawImage(context, (CGRect){.origin=CGPointZero, .size=originalSize}, currentCGImage);

    CGImageRef newCGImage = CGBitmapContextCreateImage(context);
    UIImage *newImage = [UIImage imageWithCGImage:newCGImage];

    self.imageView.image = newImage;
}

再次,儀器只能在這裏給你一個提示,問題出在哪裏,它不能告訴你確切位置的泄漏。這是惟一可以證實你在建立對象泄露的地方.你可能認爲ARC並有不多是形成代碼中內存泄漏…對不對?
回想一下,ARC只涉及Objective-C的對象。它無論理保留和的CoreFoundation對象而不是Objective-C的對象的釋放。
啊,如今它開始變得明顯的問題是什麼
-CGContextRef和CGImageRef對象永遠不會被釋放!爲了解決這個問題,在rotateTapped方法的末尾添加如下兩行代碼:

CGImageRelease(newCGImage);
CGContextRelease(context);

這兩種調用都須要來維護這兩個對象的保留計數。這個說明,你還須要瞭解引用計數 - 即便你在你的項目中使用的ARC!
從在Xcode中,使用⌘I工具構建和運行應用程序。
在使用泄漏儀器儀器再看看應用程序,看看是否泄漏的被固定。若是你正確地遵循上述步驟,泄漏應消失!


原文:How to Use Instruments in Xcode
轉載自:hufeng825.github.com 做者:神同樣的我啊

相關文章
相關標籤/搜索