iOS記憶體優化

語言: CN / TW / HK


theme: channing-cyan

記憶體優化的工具

靜態分析:Analyze

Analyze 主要分析以下四種問題:

  1. 邏輯錯誤: 訪問空指標或未初始化的變數等

  2. 記憶體管理錯誤: 如記憶體洩漏等

  3. 宣告錯誤: 從未使用的變數
  4. Api呼叫錯誤: 未包含使用的庫和框架

Instruments和Allocations

這個工具能顯示出應用的實際佔用,並可以按大小進行排序.我們只要找出哪些佔用高的,分析其原因,找到相應的解決辦法

  1. 紅色箭頭處,這種紅色X的地方就是記憶體洩漏的地方

  2. 找到上圖有個“田”樣式的圖案把那個Statictics改成Call Trees你就可以看到底部有一個Call Tree的設定。把系統方法過濾掉

活動檢測器- ActivityMonitor

執行操作(進入一個頁面退出後)-> 篩選你的APP,Memory如果不減退,則出現記憶體洩漏

殭屍物件-Zombiles

  • 殭屍物件一種用來檢測記憶體錯誤EXC_BAD_ACCESS的 物件,它可以捕獲任何嘗試訪問壞記憶體的呼叫

  • 如果給殭屍物件傳送訊息時,那麼將在執行期間崩潰和輸出錯誤日誌.通過日誌可以定位到野指標物件呼叫的方法和類名

如何開啟Zombile object檢測

1. 在Xcode中設定Edit Scheme -> Diagnostics -> Zombie Objects****

``` void printClassInfo(id obj) { Class cls = object_getClass(obj); Class superCls = class_getSuperclass(cls); NSLog(@"self:%s - superClass:%s", class_getName(cls), class_getName(superCls)); }

int main(int argc, const char * argv[]) { @autoreleasepool {

    People *aPeople = [People new];

    NSLog(@"before release!");
    printClassInfo(aPeople);

    [aPeople release];

    NSLog(@"after release!");
    printClassInfo(aPeople);
}
return 0;

} ```

檢視列印資訊

ZombieObjectDemo[1357:84410] before release! ZombieObjectDemo[1357:84410] self:People - superClass:NSObject ZombieObjectDemo[1357:84410] after release! ZombieObjectDemo[1357:84410] self:_NSZombie_People - superClass:nil

2. 從列印資訊可以看到開啟殭屍物件檢測後,People釋放後所屬類變成_NSZombie_People,如此可得物件釋放後會變成殭屍物件,儲存當前釋放物件的記憶體地址,防止被系統回收

3. 接下來開啟instruments ->Zombies,檢視dealloc究竟做了什麼.點選執行,參看Call trees.結果如下,從dealloc的呼叫知道:Zombie Objects hook 住了物件的dealloc方法,通過呼叫自己的__dealloc_zombie方法來把物件進行殭屍化。在Runtime原始碼NSObject.mm檔案中dealloc方法註釋中也有說明這一點。如下:

// Replaced by NSZombies - (void)dealloc { _objc_rootDealloc(self); }

看看物件dealloc方法呼叫棧

模擬殭屍物件的生成

``` //1、獲取到即將deallocted物件所屬類(Class) Class cls = object_getClass(self);

//2、獲取類名 const char *clsName = class_getName(cls)

//3、生成殭屍物件類名 const char *zombieClsName = "NSZombie" + clsName;

//4、檢視是否存在相同的殭屍物件類名,不存在則建立 Class zombieCls = objc_lookUpClass(zombieClsName); if (!zombieCls) { //5、獲取殭屍物件類 NSZombie Class baseZombieCls = objc_lookUpClass(“NSZombie");

//6、建立 zombieClsName 類 zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0); } //7、在物件記憶體未被釋放的情況下銷燬物件的成員變數及關聯引用。 objc_destructInstance(self);

//8、修改物件的 isa 指標,令其指向特殊的殭屍類 objc_setClass(self, zombieCls); ```

Zombile Object是如何被觸發的

再次呼叫[people release]可以看到程式斷在 ___forwarding___ ,從此處的彙編程式碼中可以看到關鍵字 _NSZombie_ ,在呼叫abort( ) 函式退出程序時會有對應的資訊輸出@"*** -[%s %s]: message sent to deallocated instance %p"。所以可以大概猜出系統是在訊息轉發過程中做了手腳。

CoreFoundation`___forwarding___: 0x7fff3f90b1cd <+269>: leaq 0x35a414(%rip), %rsi ; "_NSZombie_"

那麼我們來總結下它的呼叫過程:

``` //1、獲取物件class Class cls = object_getClass(self);

//2、獲取物件類名 const char *clsName = class_getName(cls);

//3、檢測是否帶有字首_NSZombie_ if (string_has_prefix(clsName, "NSZombie")) { //4、獲取被野指標物件類名 const char *originalClsName = substring_from(clsName, 10);

//5、獲取當前呼叫方法名  const char selectorName = sel_getName(_cmd);     //6、輸出日誌  Log(''** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);

//7、結束程序  abort(); } ```

其實Diagnostics還有一下幾個選項可以幫助我們找到app的記憶體問題

1. Enable Malloc Scribble

申請記憶體後在申請的記憶體上填0xAA,記憶體釋放後在釋放的記憶體上填0x55;再就是說如果記憶體未被初始化就被訪問,或者釋放後被訪問,就會引發異常,這樣就可以使問題儘快暴漏出來。

Scribble其實是malloclibsystem_malloc.dylib自身提供的除錯方案

2. Enable Malloc Guard Edges

申請大片記憶體的時候在前後page上加保護,詳見保護模式

3. Enable Guard Mallocs

使用libgmalloc捕獲常見的記憶體問題,比如越界、釋放之後繼續使用。

由於libgmalloc在真機上不存在,因此這個功能只能在模擬器上使用.

4. Enable Zombie Objects

Zombie的原理是用生成殭屍物件來替換dealloc的實現,當物件引用計數為0的時候,將需要dealloc的物件轉化為殭屍物件。如果之後再給這個殭屍物件發訊息,則丟擲異常,並打印出相應的資訊,除錯者可以很輕鬆的找到異常發生位置。

5. Enable Address Sanitizer (Xcode7 +)

AddressSanitizer的原理是當程式建立變數分配一段記憶體時,將此記憶體後面的一段記憶體也凍結住,標識為中毒記憶體。當程式訪問到中毒記憶體時(越界訪問),就會丟擲異常,並打印出相應log資訊。除錯者可以根據中斷位置和的log資訊,識別bug。如果變數釋放了,變數所佔的記憶體也會標識為中毒記憶體,這時候訪問這段記憶體同樣會丟擲異常(訪問已經釋放的物件)。

MLeaksFinder

騰訊開源的一款記憶體洩漏查詢工具,可以在使用APP的過程中,即時的提醒發生了記憶體洩漏

Xcode的Memory Graph

這款工具在查詢記憶體洩漏方面,可以作為MLeaksFinder的補充,用於分析物件之間的迴圈引用關係。 另外通過分析某個時刻的Live Objects,可以分析出哪些是不合理的

這個時候就進入了斷點模式,可以檢視issue面板,注意選擇右邊的Runtime:

有很多歎號說明就有問題了。看記憶體中object的名字,有一條是Closure captures leaked。展開後點擊就可以看到這個issue對應的記憶體圖形展示在中間的面板中

FBMemoryProfiler

是FaceBook出品,具體使用參考git

記憶體佔用高的原因:

使用了不合理的API

1. 對於僅使用一次或是使用頻率很低的大圖片資源,使用了[UIImage imageNamed:]方法進行載入

圖片的載入,有兩種方式,一種是[UIImage imageNamed:],載入後系統會進行快取,且沒有API能夠進行清理;另一種是[UIImage imageWithContentsOfFile:][[UIImage alloc] initWithContentsOfFile:],系統不會進行快取處理,當圖片沒有再被引用時,其佔用的記憶體會被徹底釋放掉。

基於以上特點,對於僅使用一次或是使用頻率很低的大圖片資源,應該使用後者。使用後者時,要注意圖片不能放到Assets中。

2.  一些圖片本身非常適合用9片圖的機制進行拉伸,但沒有進行相應的優化

圖片的記憶體佔用是很大的,對於適合用9片圖機制進行拉伸處理的圖片,可以切出一個比實際尺寸小的多的圖片,從而大量減少記憶體佔用。比如下面的圖片

左右兩條豎線之間的部分是純色,那麼設計在切圖時,對於這部分只要切出來很小就可以了。然後我們可以利用Xcode的slicing功能,設定圖片哪些部分不進行拉伸,哪些部分進行拉伸。在載入圖片的時候,還是以正常的方式進行載入。

3.  在沒有必要的情況下,使用了-[UIColor colorWithPatternImage:]這個方法

專案中有程式碼使用了UILabel,將label的背景色設定為一個圖片。為了將圖片轉為顏色,使用了上述方法.這個方法會引用到載入到記憶體中的圖片,然後又會在記憶體中創建出另一個影象,而影象的記憶體佔用是很大的

解決方法:此種場景下,合理的是使用UIButton,將圖片設定為背景圖。雖然使用UIButton會比UILabel多生成兩個檢視,但相比起影象的記憶體佔用,還是完全值得的。

4.  在沒有必要的情況下,使用Core Graphics API,修改一個UIImage物件的顏色

使用此API,會導致在記憶體中額外生成一個影象,記憶體佔用很大。合理的做法是:

  • 設定UIView的tintColor屬性
  • 將圖片以UIImageRenderingModeAlwaysTemplate的方式進行載入

view.tintColor = theColor; UIImage *image = [[UIImage imageNamed:name] imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate]

5.  基於顏色建立純色的圖片時,尺寸過大

有時,我們需要基於顏色創建出UIImage,並用做UIButton在不同狀態下的背景顏色.由於是純色的圖片,那麼我們完全沒有必要創建出和檢視大小一樣的影象,只需要創建出寬和高均為1px大小的影象就夠了

``` //外部應該呼叫此方法,創建出1px寬高的小影象 + (UIImage)createImageWithColor:(UIColor )color { return [self createImageWithColor: color andSize: CGSizeMake(1, 1)]; }

  • (UIImage)createImageWithColor:(UIColor)color andSize:(CGSize)size { CGRect rect=CGRectMake(0,0, size.width, size.height); UIGraphicsBeginImageContext(rect.size); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); CGContextFillRect(context, rect); UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return theImage; } ```

6. 建立水平的漸變影象時,尺寸過大

專案中有些地方基於顏色,利用Core Graphics,在記憶體中建立了水平方向從左到右的漸變影象.影象的大小為檢視的大小,這在某些檢視較大的場合,造成了不小的記憶體開銷,以在@3x裝置上一個400x60大小的檢視為例,其記憶體開銷為:

400 * 3 * 60 * 3 * 4 / 1024 = 210KB。 但是實際上這個影象,如果是400px寬,1px高,完全能達到相同的顯示效果,而其記憶體開銷則僅為: 400 * 1 * 4 / 1024 = 1.56KB

7. 在自定義的UIView子類中,利用drawRect:方法進行繪製

自定義drawRect會使APP消耗大量的記憶體,檢視越大,消耗的越多。其消耗記憶體的計算公式為: 消耗記憶體 = (width * scale * height * scale * 4 / 1024 / 1024)MB

幾乎在所有情況下,繪製需求都可以通過CAShapeLayer這一利器來實現。CAShapeLayer在CPU和記憶體佔用兩項指標上都完爆drawRect:。

其有以下優點:

  • 渲染快速。CAShapeLayer使用了硬體加速,繪製同一圖形會比用Core Graphics快很多。
  • 高效使用記憶體。一個CAShapeLayer不需要像普通CALayer一樣建立一個寄宿圖形,所以無論有多大,都不會佔用太多的記憶體。
  • 不會被圖層邊界剪裁掉。
  • 不會出現畫素化

8. 在自定義的CALayer子類中,利用- (void)drawInContext:方法進行繪製

與上一條類似,請儘量使用CAShapeLayer來做繪製。

9. UILabel尺寸過大

如果一個UILabel的尺寸,大於其intrinsicContentSize,那麼會引起不必要的記憶體消耗。所以,在檢視佈局的時候,我們應該儘量使UILabel的尺寸等於其intrinsicContentSize。 關於這一點,讀者可以寫一個簡單的示例程式,然後利用Instruments工具進行分析,可以看到Allocations中,Core Animation這一項的佔用會明顯增加。

10. 為UILabel設定背景色

如果設定的背景色不是clearColor, whiteColor,會引起記憶體開銷。 所以,一旦碰到這種場合,可以將檢視結構轉變為UIView+UILabel,為UIView設定背景色,而UILabel只是用來顯示文字。

這一點也可以通過寫示例程式,利用Instruments工具來進行驗證。

網路下載的圖片過大

幾乎所有的iOS應用,都會使用SDWebImage這一框架進行網路圖片的載入。有時會遇到載入的圖片過大的情況,對於這種情況,還需要根據具體的場景進行分析,採用不同的解決辦法。

1. 檢視很大,圖片不能被縮放

如果圖片大是合理的,那麼我們做的只能是在檢視被釋放時,將下載的圖片從記憶體快取中刪除。示例程式碼如下:

- (void)dealloc { for (NSString *imageUrl in self.datas) { NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL: [NSURL URLWithString: imageUrl]]; [[SDImageCache sharedImageCache] removeImageForKey: key fromDisk: NO withCompletion: nil]; } }

上述程式碼將使得記憶體佔用較高的情況只會出現在某個頁面中,一旦從此頁面返回,記憶體將會迴歸正常值。

2. 檢視小,這時圖片應該被縮放

如果用於顯示圖片的檢視很小,而下載的圖片很大,那麼我們應該對圖片進行縮放處理,然後將縮放後的圖片儲存到SDWebImage的記憶體快取中。

示例程式碼如下: ``` //為UIImage新增如下分類方法: - (UIImage*)aspectFillScaleToSize:(CGSize)newSize scale:(int)scale { if (CGSizeEqualToSize(self.size, newSize)) { return self; }

CGRect scaledImageRect = CGRectZero;

CGFloat aspectWidth = newSize.width / self.size.width;
CGFloat aspectHeight = newSize.height / self.size.height;
CGFloat aspectRatio = MAX(aspectWidth, aspectHeight);

scaledImageRect.size.width = self.size.width * aspectRatio;
scaledImageRect.size.height = self.size.height * aspectRatio;
scaledImageRect.origin.x = (newSize.width - scaledImageRect.size.width) / 2.0f;
scaledImageRect.origin.y = (newSize.height - scaledImageRect.size.height) / 2.0f;

int finalScale = (0 == scale) ? [UIScreen mainScreen].scale : scale;
UIGraphicsBeginImageContextWithOptions(newSize, NO, finalScale);
[self drawInRect:scaledImageRect];
UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return scaledImage;

}

  • (UIImage*)aspectFitScaleToSize:(CGSize)newSize scale:(int)scale { if (CGSizeEqualToSize(self.size, newSize)) { return self; }

    CGRect scaledImageRect = CGRectZero;

    CGFloat aspectWidth = newSize.width / self.size.width; CGFloat aspectHeight = newSize.height / self.size.height; CGFloat aspectRatio = MIN(aspectWidth, aspectHeight);

    scaledImageRect.size.width = self.size.width * aspectRatio; scaledImageRect.size.height = self.size.height * aspectRatio; scaledImageRect.origin.x = (newSize.width - scaledImageRect.size.width) / 2.0f; scaledImageRect.origin.y = (newSize.height - scaledImageRect.size.height) / 2.0f;

    int finalScale = (0 == scale) ? [UIScreen mainScreen].scale : scale; UIGraphicsBeginImageContextWithOptions(newSize, NO, finalScale); [self drawInRect:scaledImageRect]; UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();

    return scaledImage; }

//使用的地方 [self.leftImageView sd_setImageWithURL:[NSURL URLWithString:md.image] placeholderImage:[UIImage imageNamed:@"discover_position"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { if (image) { UIImage *scaledImage = [image aspectFillScaleToSize: self.leftImageView.bounds.size scale: 2]; if (image != scaledImage) { self.leftImageView.image = scaledImage; [[SDWebImageManager sharedManager] saveImageToCache: scaledImage forURL: imageURL]; } } }]; ```

第三方庫的快取機制

1.  Lottie動畫框架

Lottie框架預設會快取動畫幀等資訊,如果一個應用中使用動畫的場合很多,那麼隨著時間的積累,就會存在大量的快取資訊。然而,有些快取資訊可能以後再也不會被用到了,例如閃屏頁的動畫引起的快取。

針對Lottie的快取引起的記憶體佔用,可以根據自己的意願,選擇如下兩種處理辦法:

  • 禁止快取

[[LOTAnimationCache sharedCache] disableCaching];

  • 不禁止快取,但在合適的時機,清除全部快取,或是某個動畫的快取

``` //清除所有快取,例如閃屏頁在啟動以後不會再次訪問,那麼可以清除此介面的動畫所引起的快取。 [[LOTAnimationCache sharedCache] clearCache];

//從一個頁面返回後,可以刪除此頁面所用動畫引起的快取。 [[LOTAnimationCache sharedCache] removeAnimationForKey:key]; ```

2. SDWebImage

SDWebImage的快取機制,分為Disk和Memory兩層,Memory這一層使得圖片在被訪問時可以免去檔案IO過程,提高效能。預設情況下,Memory裡儲存的是解壓後的影象資料,這個會導致巨大的記憶體開銷。如果想要優化記憶體佔用,可以選擇儲存壓縮的影象資料,在應用啟動的地方加如下程式碼:

[SDImageCache sharedImageCache].config.shouldDecompressImages = NO; [SDWebImageDownloader sharedDownloader].shouldDecompressImages = NO;

3. 沒必要常駐記憶體的物件,實現為常駐記憶體

對於像側邊欄,ActionSheet這樣的介面物件,不要實現為常駐記憶體的,應該在使用到的時候再建立,用完即銷燬。

4. 資料模型中冗餘的欄位

對於從服務端返回的資料,解析為模型時,隨著版本的迭代,可能有一些欄位已經不再使用了。如果這樣的模型物件會生成很多,那麼對於模型中的冗餘欄位進行清理,也可以節省一定數量的記憶體佔用。

5. 記憶體洩漏

這個我就不做多餘的贅述了