iOS性能优化之内存(memory)优化

近期在工作中,对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内部访问类的实例变量

在团队中推行上述规范,可以有效的防止循环引用的发生。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • [cp][星星][星星][星星][星星][半星],汤姆汉克斯真的好适合演这种孤胆英雄啊,比如阿甘,比如这个电影里的...
    白菲Betphie阅读 244评论 0 0
  • 一 不知你是否爱过一个人 一个不能在一起的人 你爱他,总会在午夜梦回突然惊醒发呆,疯狂的想他,想为什么不能在一起,...
    情诗浅浅阅读 724评论 0 0
  • 我是蚂蚁,我很小。我不惹人注意,可我想讲一下那些你不知道的事。 现在我想跟你说的,从简单琐碎的事情说起...
    锦衣白夜行阅读 290评论 0 1
  • 昨天和今天都是有意义的日子。昨天是汶川地震十周年,今天是母亲节。 记得很清楚,汶川地震的时候我在上初二。那天有个航...
    老坑姑娘阅读 143评论 0 0