iOS性能優化之內存(memory)優化


https://www.jianshu.com/p/8662b2efbb23

 

近期在工作中,對APP進行了內存占用優化,減少了不少內存占用,在此將經驗進行總結和分享,也歡迎大家進行交流。

在優化的過程中,主要使用了以下工具:

  • Instruments和Allocations
    這個工具能顯示出應用的實際內存占用,並可以按大小進行排序。我們只要找出那些占用高的,分析其原因,找到相應的解決辦法。
  • MLeaksFinder
    騰訊開源的一款內存泄漏查找工具,可以在使用APP的過程中,即時的提醒發生了內存泄漏。
  • Xcode的Memory Graph
    這款工具在查找內存泄漏方面,可以作為MLeaksFinder的補充,用於分析對象之間的循環引用關系。
    另外通過分析某個時刻的Live Objects,可以分析出哪些是不合理的。

總結下來,主要有幾方面的原因導致內存占用高:

  • 使用了不合理的API
  • 網絡下載的圖片過大
  • 第三方庫的緩存機制
  • Masonry布局框架
  • 沒必要常駐內存的對象,實現為常駐內存
  • 數據模型中冗余的字段
  • 內存泄漏

下面從這幾方面展開討論。

1.使用了不合理的API

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

圖片的加載,有兩種方式,一種是[UIImage imageNamed:],加載后系統會進行緩存,且沒有API能夠進行清理;另一種是[UIImage imageWithContentsOfFile:][[UIImage alloc] initWithContentsOfFile:],系統不會進行緩存處理,當圖片沒有再被引用時,其占用的內存會被徹底釋放掉。

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

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

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


 
contract_right_green@3x.png

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

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

項目中有代碼使用了UILabel,將label的背景色設定為一個圖片。為了將圖片轉為顏色,使用了上述方法。這個方法會引用到一個加載到內存中的圖片,然后又會在內存中創建出另一個圖像,而圖像的內存占用是很大的。

解決辦法:此種場景下,合理的是使用UIButton,將圖片設定為背景圖。雖然使用UIButton會比UILabel多生成兩個視圖,但相比起圖像的內存占用,還是完全值得的。

1.4 在沒有必要的情況下,使用Core Graphics API,修改一個UIImage對象的顏色

使用此API,會導致在內存中額外生成一個圖像,內存占用很大。合理的做法是:

  • 設定UIView的tintColor屬性
  • 將圖片以UIImageRenderingModeAlwaysTemplate的方式進行加載
    代碼示例:
view.tintColor = theColor; UIImage *image = [[UIImage imageNamed:name] imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate] 

1.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; } 

1.6 創建水平的漸變圖像時,尺寸過大

項目中有些地方基於顏色,利用Core Graphics,在內存中創建了水平方向從左到右的漸變圖像。圖像的大小為視圖的大小,這在某些視圖較大的場合,造成了不小的內存開銷。以在@3x設備上一個400x60大小的視圖為例,其內存開銷為:
400 * 3 * 60 * 3 * 4 / 1024 = 210KB。
但是實際上這個圖像,如果是400px寬,1px高,完全能達到相同的顯示效果,而其內存開銷則僅為:
400 * 1 * 4 / 1024 = 1.56KB

1.7 在自定義的UIView子類中,利用drawRect:方法進行繪制

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

幾乎在所有情況下,繪制需求都可以通過CAShapeLayer這一利器來實現。CAShapeLayer在CPU和內存占用兩項指標上都完爆drawRect:。
其有以下優點:

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

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

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

1.9 UILabel尺寸過大

如果一個UILabel的尺寸,大於其intrinsicContentSize,那么會引起不必要的內存消耗。所以,在視圖布局的時候,我們應該盡量使UILabel的尺寸等於其intrinsicContentSize
關於這一點,讀者可以寫一個簡單的示例程序,然后利用Instruments工具進行分析,可以看到Allocations中,Core Animation這一項的占用會明顯增加。

1.10 為UILabel設定背景色

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

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

2.網絡下載的圖片過大

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

2.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.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]; } } }]; 

3.第三方庫的緩存機制

3.1 Lottie動畫框架

Lottie框架默認會緩存動畫幀等信息,如果一個應用中使用動畫的場合很多,那么隨着時間的積累,就會存在大量的緩存信息。然而,有些緩存信息可能以后再也不會被用到了,例如閃屏頁的動畫引起的緩存。

針對Lottie的緩存引起的內存占用,可以根據自己的意願,選擇如下兩種處理辦法:

  • 禁止緩存
[[LOTAnimationCache sharedCache] disableCaching]; 
  • 不禁止緩存,但在合適的時機,清除全部緩存,或是某個動畫的緩存
//清除所有緩存,例如閃屏頁在啟動以后不會再次訪問,那么可以清除此界面的動畫所引起的緩存。 [[LOTAnimationCache sharedCache] clearCache]; //從一個頁面返回后,可以刪除此頁面所用動畫引起的緩存。 [[LOTAnimationCache sharedCache] removeAnimationForKey:key]; 

3.2 SDWebImage

SDWebImage的緩存機制,分為Disk和Memory兩層,Memory這一層使得圖片在被訪問時可以免去文件IO過程,提高性能。默認情況下,Memory里存儲的是解壓后的圖像數據,這個會導致巨大的內存開銷。如果想要優化內存占用,可以選擇存儲壓縮的圖像數據,在應用啟動的地方加如下代碼:

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

3.3 YYModel

這個庫很優秀,速度快,使用方便。但是凡事都有兩面性,其在內部緩存了類信息,類的屬性信息等內容,且沒有提供公開的API來清理緩存。這會導致這些緩存會一直存在,特別是當一個頁面返回時,其引起的內存開銷無法被釋放。

所以,如果想要優化內存,建議從項目中移除這個框架,改為手動解析。雖然寫的時候稍微多花一些時間,但是在CPU和內存性能上,都是最高的。

4.Masonry布局框架

這個框架幾乎是每個APP都引入並大量使用的,其確實很優秀,但也存在一些問題:

  • 如果沒有superView,或某個參數為nil時,容易導致崩潰。
  • 在實現過程中,會創建出很多的小的對象,比基於frame的布局開銷大很多。

所以,我的想法是,此框架可以用,但應該減少其使用,尤其是在一些不會被釋放的頁面中,更是應該不用或少用,因為其帶來的內存開銷,無法被釋放。

5.沒必要常駐內存的對象,實現為常駐內存

對於像側邊欄,ActionSheet這樣的界面對象,不要實現為常駐內存的,應該在使用到的時候再創建,用完即銷毀。

6.數據模型中冗余的字段

對於從服務端返回的數據,解析為模型時,隨着版本的迭代,可能有一些字段已經不再使用了。如果這樣的模型對象會生成很多,那么對於模型中的冗余字段進行清理,也可以節省一定數量的內存占用。

7.內存泄漏

內存泄漏會導致應用的內存占用一直升高,且無法降低。在實際工作中的痛點是:前腳修復了內存泄漏,后腳又有開發者不小心在block里寫了self,或是引用了instance variable,從而再次導致內存泄漏的發生。

基於此,在項目中引入ReactiveObjC中的兩個牛X的宏,@weakify, @strongify,並遵循以下寫法規范:

  • 在block外部使用@weakify(self),可以一次定義多個weak引用。
  • 在block內部的開頭使用@strongify(self),可以一次定義多個strong引用。
  • 在block內部使用self編寫代碼
  • 嚴禁在block內部訪問類的實例變量

在團隊中推行上述規范,可以有效的防止循環引用的發生。



作者:buptwsg
鏈接:https://www.jianshu.com/p/8662b2efbb23
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM