1. 程式人生 > >Allocations分析記憶體分配

Allocations分析記憶體分配

Allocations

Allocations用來分析靜態記憶體分配。

Demo App

Demo是一個簡單的圖片應用:首頁只有一個簡單的入口;次級頁面會讀取本地圖片,加濾鏡,然後按照瀑布流的方式顯示出來;第三個頁面提供大圖顯示;

執行程式碼:點選Photos -> 進入main -> 點選一張圖,進入詳情頁 -> Pop直到回到第一個頁面,重複這段操作,從XCode的記憶體模組發現兩個問題。

  1. 記憶體峰值過大
  2. 退出介面後,記憶體沒有降低。

Statistics

選單欄選擇Product -> Profile,然後選擇Allocations,執行專案,按照記憶體飆升的路徑重複操作,採集到記憶體資料後停止執行。

預設看到的資料是Statistics(靜態分析),點選mark 2可以更改資料分析模式。

為了更好的解決問題,有必要講解下這裡的記憶體相關概念:

  • All Heap Allocation 堆上malloc分配的記憶體,不包過虛擬記憶體區域。
  • All Anonymous VM 匿名的虛擬記憶體區域。何為匿名呢?就是Allocations不知道是你哪些程式碼建立的記憶體,也就是說這裡的記憶體你無法直接控制。像memory mapped file,CALayer back store等都會出現在這裡。這裡的記憶體有些是你需要優化的,有些不是。

表格的每一列的資料解釋:

列名 含義
Graph 是否需要繪製出來(在mark 1可以看到)
Category 類別,比如CF物件,OC物件,或者就是一塊原始的記憶體。
Bytes 大小
Count 個數
Persistent Bytes 沒有釋放的大小
# Persistent 沒有釋放的個數
Transient Bytes 已經釋放的大小
# Transient 已經釋放的個數
Total Bytes Persistent Bytes + Transient Bytes
# Total # Transient + # Persistent
Events 所有型別的事件數量
Transient/Total Bytes 柱狀圖顯示百分比

我們勾選前四個Graph,通過曲線的趨勢,不難看出問題就出在VM CoreImage上:

我們點一下mark 4,進入詳情頁,然後選擇一個記憶體物件地址,在右側我們可以看到這個物件是如何被建立的:

雙擊這一行,進入彙編介面,可以看出來,最後記憶體是由mmap分配的:

也可以雙擊右側Stack Trace,看看自己的程式碼:

Generation

利用Generation,我們可以對記憶體的增量進行分析:時間戳B相比時間戳A有那些記憶體增加了多少

點最下面的Mark Generation會建立一個Generation,並且在圖形區域增加一個小紅旗。

點Statictis,從下拉列表中,選擇Generations,我們就可以看到記憶體增量主要在VM: CoreImage中,這裡的每一行也可以點選檢視詳情,或者在右側檢視棧:

Call Tree

按照類似的方式,這次我們選擇Call Tree來直接分析程式碼是如何建立記憶體的,勾選Separator By Thread,按照執行緒來對記憶體進行分類:

Tips:按住Option,然後滑鼠左鍵點選圖中箭頭指向的三角箭頭,可以快捷展開呼叫棧。

也可以勾選:

  • Invert Call Tree 倒置函式棧
  • Hide System Libraries 隱藏系統庫

這樣,我們過濾掉了系統符號,同時也用更直觀的方式看到了呼叫棧:

我們同樣可以選擇一行雙擊,然後可以進入XCode檢視原始碼或者彙編程式碼:

可以看到,引起記憶體爆增的就是這段程式碼

- (UIImage *)filterdImage:(UIImage *)originalImage{
    CIImage *inputImage = [CIImage imageWithCGImage:originalImage.CGImage];
    CIFilter *filter = [CIFilter filterWithName:@"CIColorMonochrome"];
    [filter setValue:inputImage forKey:kCIInputImageKey];
    [filter setValue:[CIColor colorWithRed:0.9 green:0.88 blue:0.12 alpha:1] forKey:kCIInputColorKey];
    [filter setValue:@0.5 forKey:kCIInputIntensityKey];
    CIContext *context = [CIContext contextWithOptions:nil];
    CIImage *outputImage = filter.outputImage;
    CGImageRef image = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage * filterImage =  [UIImage imageWithCGImage:image];
    return filterImage;
}

Allocations list

Allocations List提供了一種更純粹的方式,讓你看到記憶體的分配的列表,我們一般會選擇記憶體從高到低,看看是不是有什麼意外分配的大記憶體塊:

可以看到,排名前幾的記憶體塊都是VM:CoreImage,從名字也就不難看出來,這是圖片引起的記憶體。

我們選中某一行,在右側可以看到具體的呼叫棧:

解決記憶體問題

不管是上述那種分析方式,我們都很容易找到問題出現在這段程式碼裡:

那麼,為什麼這段程式碼分配記憶體後沒有釋放呢?如果有一些CoreFoundation或者CoreGraphics經驗,很容易就知道這裡應該手動釋放記憶體,這裡假設你不知道,那麼怎麼找到原因呢?

看看這個函式的文件文件就知道了:

- (nullable CGImageRef)createCGImage:(CIImage *)image
                            fromRect:(CGRect)fromRect

Creates a Quartz 2D image from a region of a Core Image image object.
Renders a region of an image into a temporary buffer using the context, then creates and returns a Quartz 2D image with the results.

You are responsible for releasing the returned image when you no longer need it.

所以,在建立函式後,我們進行release即可

CGImageRef image = [context createCGImage:outputImage fromRect:outputImage.extent];
UIImage * filterImage =  [UIImage imageWithCGImage:image];
CGImageRelease(image);

再觀察記憶體:

可以看到,仍然有個峰值,我們的瀑布流介面其實並不需要完整的大圖塞給ImageView,一個比較常見的優化方式是對圖片進行縮放,這裡有兩點要注意

  1. 大圖的縮放不要用UIGraphicsBeginImageContextWithOptions或者CGBitmapContextCreate,因為當圖片很大的時候,這個函式很有可能建立幾百M甚至上G的記憶體,應該用更底層的ImageIO相關的API
  2. 假如ImageView的尺寸是100*100,那麼為了不影響使用者體驗,你應該縮放到100*UIScreem.main.scale

縮放舉例:

- (UIImage *)scaledImageFrom:(NSURL *)imageUrl width:(CGFloat)width{
    CGImageSourceRef source =  CGImageSourceCreateWithURL((__bridge CFURLRef)imageUrl, nil);
    CFDictionaryRef options = (__bridge CFDictionaryRef) @{
                                                           (id) kCGImageSourceCreateThumbnailWithTransform : @YES,
                                                           (id) kCGImageSourceCreateThumbnailFromImageAlways : @YES,
                                                           (id) kCGImageSourceThumbnailMaxPixelSize : @(width)
                                                           };
    
    CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, options);
    UIImage *scaled = [UIImage imageWithCGImage:scaledImageRef];
    CGImageRelease(scaledImageRef);
    return scaled;
}