iOS内存优化

内存优化工具

  • Instruments的Allocations

这个工具能显示出应用的实际内存占用,并可以按大小进行排序。我们只要找出那些占用高的,分析其原因,找到相应的解决办法。

  • 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片图机制进行拉伸处理的图片,可以切出一个比实际尺寸小的多的图片,从而大量减少内存占用。比如下面的图片:

image.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.内存泄漏

内存泄漏会导致应用的内存占用一直升高,且无法降低。

7.1内存泄露的原因

  • block 循环引用
  • NSTimer 未释放
  • 通知未能正确的移除

7.2 block循环引用的处理

基于此,在项目中引入ReactiveObjC中的两宏,@weakify, @strongify,并遵循以下写法规范:

在block外部使用@weakify(self),可以一次定义多个weak引用。
在block内部的开头使用@strongify(self),可以一次定义多个strong引用。
在block内部使用self编写代码
严禁在block内部访问类的实例变量
在团队中推行上述规范,可以有效的防止循环引用的发生。

7.3内存泄漏的查找——使用 Debug Memory Graph

配置

image.png

如果没有勾选 Malloc Stack 在调试的时候,在右侧是看不到调用的堆栈信息。

  1. 勾选 Malloc Stack 之后内存会相应的增高,如果不调试可以关闭该选项
  2. 建议选择 Live Allocations Only如果选择All Allocations and Free History 会出现一些额外的影响因素

打开方式

image.png

或者
Debugger Navigator -> View Memory Graph Hierarchy
image.png

分析内存泄露
image.png

如上图示,括号中的数字代表该类在内存中实例的数量。点击箭头所指的感叹好小图标,会显示出内存泄露的类。如果没有也不要高兴的太早,括号中数字比较大类,可能是本该释放却没有释放的类。如 A 类页面进入到B 类页面,返回之后查看内存图谱, 如果 B 类依旧在列表中,就需要具体分析是否 B 类中内存泄露无法释放,下图为点击该类的堆栈信息。


image.png

你可能感兴趣的:(iOS内存优化)