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片圖機制進行拉伸處理的圖片,可以切出一個比實際尺寸小的多的圖片,從而大量減少內存占用。比如下面的圖片:

左右兩條豎線之間的部分是純色,那么設計在切圖時,對於這部分只要切出來很小就可以了。然后我們可以利用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(