Canoe

项目内存优化

2018.03.16

因为arc的缘故,现在很多的开发人员会将注意力放在业务模块,以至于疏忽了内存部分的优化,刚开始看不出什么问题,当上线之后或者项目慢慢庞大的时候,会出现各种各样的问题,因为内存问题相对于一般的问题来说,比较难以定位和查找,所以需要我们时时注意,在每一个小功能点做好内存控制。
我总结了一些在项目开发过程中,我所做的一些优化以及需要注意的点。


一、检测内存问题

1、xCode Memory Report

通过xCode的图形展示工具可以在运行时看到内存的占用,在进入特殊页面内存不正常的增长时就需要对内存进行分析了,当退出某一个页面内存没有得到释放那么该页面可能没有销毁。

2、系统静态检测

xCode提供静态分析功能,点击product,然后点击Analyze就可以进行静态分析,使用简单,并且能够在编译过程中进行内存分析,找出代码中潜在的内存泄漏隐患,不需要实际的运行。

Analyze作用

  • 分析上下语句的逻辑,检查逻辑缺陷。
  • 检测一些内存泄漏
  • 检测没有使用的变量
  • 没有遵循项目使用框架和库,错误的使用API

但是使用静态分析需要自己仔细判断是否真的存在内存泄漏,而且在使用时会发现整个项目编译下来出现很多的可能出现内存泄漏的地方,包括很多三方库,其中很多对于我们检测内存泄漏都是没有意义的,所以静态分析在一般使用中只能作为一种参考。

官方文档:Appendix: The Static Analyzer

3、instruments检测内存

instruments是苹果提供的性能分析工具,我们可以使用它来很好的分析应用内存。主要是使用Allocations和Leaks来分析内存和查看内存泄漏,具体的使用方式网上已经有很多资料,也可以参照下面的链接。

需要注意的是,内存泄漏有两种类型:

  1. 真正的内存泄漏是指一个对象不再需要使用了,没有得到释放,以至于这一块内存永远不能重用,即使是使用ARC,也很常见这种类型的错误,例如循环引用。
  2. 无限的内存增长,某一个对象在不断的创建,或许内存占用并不大,或许只在进入某一个页面会出现这种增长,但是这种情况不断的持续,在某一个时候,app的内存达到极限,会被系统杀掉。

4.第三方工具

  1. MLeaksFinder

微信读书团队开源的一个检测UI方面泄漏的工具,更加贴合项目需要,可以解决很大部分的问题。集成简单,在新功能开发时可以很好的检测bug及时解决。

  1. FBRetainCycleDetector

Facebook 在前阵子开源了一个循环引用检测工具 FBRetainCycleDetector。当传入内存中的任意一个 OC 对象,FBRetainCycleDetector 会递归遍历该对象的所有强引用的对象,以检测以该对象为根结点的强引用树有没有循环引用。
FBRetainCycleDetector 的使用存在两个问题:

  • 需要找到候选的检测对象
  • 检测循环引用比较耗时

正是由于这两个问题,FBRetainCycleDetector 通常是结合其它工具一起使用,通过其它工具先找出候选的检测对象,然后进行有选择的检测。当 MLeaksFinder 与 FBRetainCycleDetector 结合使用时,能达到很好的效果。先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。

相关文档


二、解决内存问题

1、一些常见的内存泄漏

1.1 NSTimer循环引用

基于平时使用NSTimer,我们分析一下NSTimer的强引用。

// 标记1
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    // 标记2
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 标记3
    self.timer = timer;

第一步:新建timer,timer对self进行强引用。
第二步:NSRunLoop对timer进行强引用。
第三步:self对timer进行强引用。

当停止计时时,必须要调用invalidate方法,这是唯一将timer从runloop中移除的方法,同时他会释放timer强引用的对象,也就是将第二步和第三步的的强引用去除,需要注意的是一定要在同一个线程调用invalidate方法,在另一个线程调用将会失效。

1.2 block的循环引用

在使用 block 的时候,为了避免产生循环引用,通常需要使用 weakSelf 与 strongSelf,写下面这样的代码:

__weak typeof(self) weakSelf = self;
[self doSomeBlockJob:^{
}];

不过需要注意的是当 block 本身不被 self 持有,而被别的对象持有,同时不产生循环引用的时候,就不需要使用 weak self 了。最常见的代码就是 UIView 的动画代码,我们在使用 UIView 的 animateWithDuration:animations 方法做动画的时候,并不需要使用 weak self,因为引用持有关系是:

  • UIView 的某个负责动画的对象持有了 block
  • block 持有了 self

还有一种情况是为了避免在block的执行过程中,self被突然释放,我们需要在内部写一个strongself,保证在block执行完之后self才会得到释放。

2、imageWithContentsOfFile

  • Resource图片加载方式

resource图片加载方式是指使用imageWithContentsOfFile:创建图片的图片管理方式.
导入方式
直接将图片拖入目录中。
使用方式

NSString *path = [NSBundle.mainBundle pathForResource:@"image@2x" type:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path];

原理
就是通过文件路径将图片转化成data数据,将data数据再通过图片加载出来显示。
在 Resource 的图片管理方式中, 所有的图片创建都是通过读取文件数据得到的, 读取一次文件数据就会产生一次NSData以及产生一个UIImage, 当图片创建好后销毁对应的NSData, 当UIImage的引用计数器变为0的时候自动销毁UIImage. 这样的话就可以保证图片不会长期地存在在内存中.
一般Resource图片都是数据较大的图片,并且不需要使用多次的图片,例如引导页,启动页面等。

  • ImageAssets 图片加载方式

ImageAssets图片加载方式是为了适配不同比例的屏幕,在工程中导入两张或者三张不同像素的图片,在使用的时候会根据不同的屏幕来获取不同大小的图片。
导入方式
放进ImageAssets文件夹
使用方式

UIImage *image = [UIImage imageNamed:@"image"];

原理
也是将图片数据转化成UIImage,只不过这些图片数据都打包在 ImageAssets 中。还有一个最大的区别就是图片缓存。相当于有一个字典, key 是图片名, value是图片对象。调用imageNamed:方法时候先从这个字典里取, 如果取到就直接返回, 如果取不到再去文件中创建, 然后保存到这个字典后再返回。由于字典的key和value都是强引用, 所以一旦创建后的图片永不销毁。
一般ImageAssets图片都是icon类型的图片,大小在3-20kb。

  • 解决方案

项目的启动动画使用的是多张连续的图片组合成Gif动画,使用的是定时器定时切换图片播放,完成之后从界面上移除。
但是在使用时用的是imageNamed的加载方式,改成imageWithContentsOfFile,在动画播放完成之后将imageView.image置为nil,再次运行之后内存降了8M左右。

3、SDWebImage内存暴涨

在进入app之后,加载图片内容列表,使用instruments检测时发现内存暴涨。

在SDWebImage的issue中找到了解决方案。Memory problem or leak on iOS 7 ? · Issue #538 · rs/SDWebImage · GitHub

  • 原因

在源文件的描述中发现SDImageCache维护一个内存缓存和一个可选的磁盘缓存。磁盘缓存异步执行写操作所以不会对UI增加不必要的延迟。属性shouldCacheImagesInMemory默认为YES,shouldDecompressImages默认也为YES。而shouldDecompressImages属性会对下载和缓存的图片进行解压缩来提高性能,但是会消耗很多内存,并且默认为开启,问题就出在这里,当我们大量在线加载图片的时候会使内存过度消耗。

  • 解决方案
  • 将shouldDecompressImages参数设置为NO。
[SDImageCache sharedImageCache].shouldDecompressImages = NO;
[SDWebImageDownloader sharedDownloader].shouldDecompressImages = NO;
  1. 在ViewController的viewDidDisappear方法中clearMemory,如果使用BaseViewController,可以在加到基类的viewDidDisappear方法中。
[[SDImageCache sharedImageCache] clearMemory];
  1. 在MemoryWarning的时候clearMemroy,可以加到AppDelegate中。
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
    [[SDImageCache sharedImageCache] clearMemory];
}

三、预防内存问题

大部分的内存问题在我们能力提升的过程中可以做到慢慢减少,但是如果不注意这一块,随着项目的迭代,内存依旧会有很大的优化空间,我将根据我的一些了解提出一些建议。

1、代码重构

这可能是很多人不愿意去修改的一块,看到逻辑混乱,历史遗留的代码就头痛,不愿意去触碰,然而很多的问题就是在需求的不断修改中产生的,新的需求更迭,老的需求的代码依然累计在项目中,虽然针对新的流程做了修改,但是依然有很多废弃代码。
在我们觉得时间充裕的时候,单独针对一个模块开一个分支进行一次重构,寻找最优化的设计,重构即有利于加深对项目业务流程的理解,也有利于个人的技能成长,虽然过程是艰难的,但是当重构完成之后,自己会得到较大的提升,并且你会发现新的方案对比旧的方案可能更优雅高效,何乐而不为呢。

2、预处理和延时加载

预处理,是将初次显示需要耗费大量线程时间的操作,提前放到后台线程进行计算,再将结果数据拿来显示。
延时加载,是指首先加载当前必须的可视内容,在稍后一段时间内或特定事件时,再触发其他内容的加载。这种方式可以很有效的提升界面绘制速度,使体验更加流畅。例如在应用程序刚开始运行的时候,很多的请求以及逻辑我们可以延时调用,优化程序的启动时间。
这两种方法都是在资源比较紧张的情况下,优先处理马上要用到的数据,同时尽可能提前加载即将要用到的数据。

3、使用正确的API

  • 选择合适的容器,能够使用UITableView重用cell的地方就尽量使用UITableView;
  • 选择正确的存储方式,在使用缓存时会有性能消耗的问题,而且要注意线程安全问题,一个原则就是缓存所需要的,不大可能改变但是经常需要读取的东西。
  • 正确的选择imageNamed:和imageWithContentsOfFile:方法
  • 在真正需要打印的地方才使用NSLog()
  • 当试图获取磁盘中一个文件的属性信息时,使用 [NSFileManager attributesOfItemAtPath:error:] 会浪费大量时间读取可能根本不需要的附加属性。这时可以使用 stat 代替 NSFileManager,直接获取文件属性:
#import <sys/stat.h>
struct stat statbuf;
const char *cpath = [filePath fileSystemRepresentation];
if (cpath && stat(cpath, &statbuf) == 0) {
    NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size];
    NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime];
    NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime];
}

4、定期更新三方库

最近解决的很多问题都是因为第三方的一些问题导致的,如果发现第三方的原因,首先在github的issue中寻找有没有人提出相同的问题,如果没有可以及时在github上反馈,还有一种情况是github上有人提出了解决方案,但是库开发者并没有及时解决并更新代码,这时候我们可以先fork一份代码到自己的库中,然后在库中进行修改,之后将pod源对应到自己的库,例如解决GPUIamge录制视频时前后几帧出现黑屏的问题

5、使用工具辅助检测内存泄漏

5.1 MLeakFinder

微信读书团队在github开源的一款内存泄露检测工具,具体原理和使用方法可以参见这篇文章。内存泄露引起的性能问题是很难被察觉的,只有泄露到了相当严重的程度,然后通过Instrument工具,不断尝试才得以定位。MLeakFinder能在开发阶段,把内存泄露问题暴露无遗,减少了很多潜在的性能问题。

5.2 facebook三件套

Comments
Write a Comment