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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的iOS性能优化之内存(memory)优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 说说前端开发中的SEO
- 下一篇: 明星基金经理 基金经理有哪些