复盘一次内存优化
背景
最近有用户反馈,App在进入后台后经常会被杀死,需要重新启动,对于用户来说,这样的体验无疑非常糟糕。当App内存消耗比较大,切换到其他App时,即使其他App向系统申请的内存不是特别大,系统也会因为资源紧张,优先把内存消耗较多的App回收,这就出现了用户反馈的问题。要想解决这个问题,就需要对App的内存进行优化。
常见的内存问题有哪些?
- 内存泄漏:申请的内存空间使用完毕之后未回收
一次两次的内存泄漏危害还好,但是如果一直泄漏,内存迟早要被消耗光,导致App崩溃,因此在日常开发过程中一定要避免内存泄漏
- 内存常驻:内存不合理运用,存在已分配内存的引用,但实际中程序不会再使用。
因为一些原因,有些对象在App的整个生命周期中常驻,比如单例,如果不善加管理,任由这些内存常驻,App的内存就会一直居高不下,这类问题很难利用工具直接定位,需要我们结合代码分析
- 内存峰值过大:某个场景下,内存消耗大幅增加,产生一个较高的内存峰值
某些场景可能因为代码的原因,导致内存瞬间大幅增加,这时就需要针对性分析,排查内存增加的原因,并进行优化
- 内存溢出:申请内存时,系统没有足够的内存空间供其使用,导致App崩溃
当内存消耗一直比较高,新申请的内存又比较大,就有可能导致内存溢出,这时App就会被系统杀死,也就是OOM
优化思路
1. 利用Instrument分析内存
Instrument是苹果提供的分析工具,可以利用Instrument分析内存分配和内存泄漏的情况。网上关于Instrument使用的教程非常多,这里我总结几点注意点。
- 利用Instrument分析内存主要依赖
Allocations和Leaks两个选项,分别对应内存分配和内存泄漏 Allocations分析内存时,总的内存占用 =All Heap Allocations+All Anonymous VM:All Heap AllocationsApp运行在堆上的内存,对应着由代码生成的各种实例对象All Anonymous VM:匿名的虚拟内存,这里包含一些系统模块的内存占用,也有一些保证我们代码正常运行不可缺少的部分- CG raster data(光栅化数据,也就是像素数据。注意不一定是图片,一块显示缓存里也可能是文字或者其他内容。通常每像素消耗 4 个字节)
- Image IO(图片编解码缓存)
- Stack (每个线程都会需要500KB左右的栈空间)
- CoreAnimation
- SQLite
- Network
- 如果所有的calltree显示的都是地址时,可以确认一下
Debug Information Format是不是选择的DWARF with dSYM File - Instrument每一列的数据解释 | 列名 | 含义 | | :---: | :---: | | Graph | 是否选择要绘制对应Category的走势图 | | Category | 类别,真实内存、虚拟内存等 | | Persistent Bytes | 没有释放的内存大小 | | # Persistent | 没有释放的内存个数 | | # Transient | 已经释放的内存个数 | | Total Bytes | 累计的内存大小 | | # Total | 累计的内存个数 | | Bytes Used | 占用的字节大小 | | Count | 申请内存的次数 | | Symbol Name | 调用栈信息 |
我这边经过使用Instrument对内存的分析,很快就定位到了 Kingfisher 这个库。接下来,重点调研 Kingfisher
2. Kingfisher内存优化
首先查看Kingfisher有关内存问题的issues,通过查看这些issues终于明白了,原来Kingfisher V4版本在设计之初选择了一个相对比较激进的处理图片内存缓存的方法,让图片在内存中无限制的缓存,直到遇到内存警告时再进行清理。通常情况下,这个策略可以很好的工作,但是有些情况下系统并不会及时、准确的传递内存警告,这就会导致App有概率被杀死,尤其在一些低端设备上。在Kingfisher V5版本,喵神已经调整了策略,针对Kingfisher V4版本也可以通过设置最大缓存值来限制。
下面是喵神的原话:
In Kingfisher 4, by default, the memory cache was using a "greedy" way to accept images. It will keep sending images to memory cache without an upper limitation, until a
.didReceiveMemoryWarningNotificationreceived. As soon as the system detects the memory availability level is low, Kingfisher will free up the used memory to make the system happy. This mechanism worked well, at least for a time. We received reports on users apps crash due to memory pressure recently. After some investigation, we found that sometimes, the system won't deliver the.didReceiveMemoryWarningNotificationcorrectly. It, in turn, makes Kingfisher think there is still plenty of memory to use. But it is not the truth. This problem can be fixed by limiting the max memory cache size in Kingfisher. But since the default behavior is "no limit", so it keeps happening for new users of Kingfisher. In Kingfisher 5, we use a more conservative strategy by default. Now a maximum of 25% device memory would be used as the memory cache.
因为Kingfisher V5需要从iOS 10开始支持,我们项目需要支持iOS 9系统,所以我就基于Kingfisher V4进行优化。
2.1 限制图片缓存大小
正如喵神在文档中讲的,Kingfisher V4版本可以通过限制图片缓存的大小来避免内存增长过大的情况。为了能充分利用各个型号设备的内存,我这里参考了Kingfisher V5的策略,限制图片缓存大小为当前设备最大物理内存的5%。
let totalMemory = ProcessInfo.processInfo.physicalMemory
let costLimit = totalMemory / 20
let costMemory = (costLimit > UInt.max) ? UInt.max : UInt(costLimit)
ImageCache.default.maxMemoryCost = costMemory
2.2 及时停止不必要的图片请求
Kingfisher提供了 cancelDownloadTask 方法,可以在一些合适时机主动停止图片的下载请求,比如对于列表页面,可以通过实现 didEndDisplaying 代理回调,在回调中执行 cell.imageView.kf.cancelDownloadTask() 来停止图片下载。
3. 内存常驻排查
3.1 使用懒加载方式延迟加载一些不需要立即使用的对象或资源
结合代码分析项目中是否存在一些对象或资源并不需要立即创建,将这些对象或资源采用懒加载的方式在需要的时候再进行加载,比如我们项目中在进入App后不仅加载了书城,同时还加载了福利页面,事实上福利页面是一个WebView页面相对比较消耗内存,而用户不一定需要进入福利页面,这就增加了不必要的内存消耗。
3.2 避免单例滥用
单例对象在整个App的生命周期内都存活,结合代码分析项目的单例是否一定要存在,单例中是否存在一些不必要的对象,能否将它们移除。
3.3 图片加载优化
- 避免使用
[UIColor colorWithPatternImage:]方法
[UIColor colorWithPatternImage:] 这个方法会引用一个加载到内存的图片,同时在内存中创建出另一个对象,可以使用 self.view.layer.contents = (id) image.CGImage; 替换,以优化内存。
- 善用
imageNamed:和contentsOfFile:方法
这两个方法都可以加载图片,区别是 imageNamed: 会在内存中缓存图片资源, contentsOfFile: 不会缓存图片资源,因此,根据实际情况合理选择加载方式,有助于内存优化。
3.4 删除冗余的模型字段
随着版本迭代,有些接口字段已经不再使用,这时及时将这些字段移除,有助于减少这些模型占用的内存空间,积少成多,优化内存。
4. 分析对比优化效果
完成优化工作后,还需要对比优化前后的内存,用数据来表现优化的效果。这时可以利用 Instrument 的 Mark Generation 功能,分别统计每个功能模块优化前后的内存增长。 经过统计对比,我们项目在阅读器翻页过程中的性能优化最明显,达到了60%。这是因为我们在阅读器中插入了大量的广告图片,如果按照Kingfisher的默认缓存设置,这些图片会一直保留在内存中,等待内存警告或进入后台时才会释放,限制了缓存最大值后,这个现象自然就得到了大幅度的优化。