iOS单例ViewController与UIImage对象内存优化

文章目录
  1. 问题严重性
  2. 分析内存大户
  3. 回收视图
  4. 加载图片的正确姿势
  5. 善后
  6. 结果

用Swift做了一个登录界面。背景是一个全屏幕的UIImageView,用定时器来定时更换图片,点击登录/注册按钮会出现UITextField。为了使用方便简单,比如用户退出登录时、用户令牌超期等情况,直接弹出登录界面——我把这个登录ViewController做成了单例模式。

本文由Swift语言做示例,由于Objective-C和Swift均使用ARC方式管理内存,所以优化思路和方式完全相同,只是相关方法的使用稍有不同。

对于单例模式想必各位开发者并不陌生,优点是有的,缺点也是非常明显的,在整个应用的生命周期中不会被销毁重生。这就造成了一个问题,资源浪费——显然用户不会经常使用登录视图控制器的。那这么说来是不是就没有办法了呢,当然不是,经过我的调试,显著减少了单例模式的内存消耗。

问题严重性

先看看不经过优化,问题有多严重。

/illustrations/2016-03-30-iOS-Singleton-ViewController-Performance-optimization/01.jpg

五张图,居然吃掉了26MB+的内存!你会好奇我放了什么图,其实就是五张分辨率一般普通图片而已,在真机调试时,这个问题并不明显,然而在模拟器上问题会被放大。这个原因目前不明, 有机会要了解一下。

/illustrations/2016-03-30-iOS-Singleton-ViewController-Performance-optimization/02.jpg

其实就是几百K的jpg图片。

但如果是一张图片几兆呢,可以压缩,那如果图片特别多呢?更何况这是运行在单例模式下的视图控制器,所以必须要做性能优化。

按照本文所述的几种方式进行优化,能够保证单例模式视图控制器不展现在当前屏幕上时,保持较低内存。

分析内存大户

擒贼先擒王,内存消耗小的对象我们可以不管他,抓住重点,释放掉内存大户就是我们优化的主要目标。

开始运行应用,在Xcode左侧Navigator中找到Debug Navigator,我们会看到几条绵延的图表,找到memory双击,选择Profile in Instruments,点击Transfer,我们会进入到一个Instruments的内存调试实例中,屏幕上部的柱状图表会显示该应用当前的在堆中占用内存的大小。下部分的列表显示了具体是哪些东西吃掉了多少内存。

点击下部列表的Persistent Bytes表头,来以该列作为索引由大到小排序。表格前三行分别是应用总体内存消耗量,我们不用管,直接看第四行开始,那就是我们需要重点优化的项目。目前还没进入消耗内存最大的ViewController,所以我们看到的都是些小兵小卒。

现在present进我们的内存大户ViewController,一下子就会看到几个兆级的内存消耗,马上看到排第一的就是ImageIO_jpeg_Data,点击这一项目Category中圆形按钮,我们会看到内存的具体去向,我们共有五张图,和这里五个项目一一对应,第二张图分辨率最大,所以消耗内存最大,达到了16MB。所以现在我们就找到了对应该优化的项目了。

回收视图

在适当的时候去调用self.view = nil,如果没有其他指针指向self.view,那么这块内存区域就会被释放。

『适当的时候』对于单例模式视图控制器来说,就是该VC不在屏幕上的时候。熟悉视图控制器生命周期的同学们应该会马上想到在viewDidDisappear方法中添加相关方法,但请注意,这样会导致dismiss的动画失效,因为没等动画开始,就过早地将相关view释放了。所以,这里我们包装了dismissViewControllerAnimated方法,在该方法的completion闭包中添加移除视图的方法,代码如下:

1
2
3
4
5
6
7
8
9
func dismiss() {
dismissViewControllerAnimated(true) {
for v in self.view.subviews {
v.removeFromSuperview()
}
self.view = nil
self.backgroundArray = []
}
}

别忘了把内存大户,背景图的数组清空。

现在打开Instrument查看情况,并没有发现什么变化。什么原因呢,我们接着看。

加载图片的正确姿势

我们在背景图数组的初始化的地方,用了最常见的UIImage(named:)构造方法来创建。UIImage还有其他初始化方法UIImage.init(contentsOfFile:)UIImage(data:),殊不知用不同的方式来创建图像,会产生iOS对内存管理的极大区别。

在苹果提供的文档中,对这一问题解释的很清楚了:

If you have an image file that will only be displayed once and wish to ensure that it does not get added to the system’s cache, you should instead create your image using imageWithContentsOfFile:. This will keep your single-use image out of the system image cache, potentially improving the memory use characteristics of your app.

通过UIImage(named:)读到的图片,直接进入缓存区,并且这块缓存区的数据,并不会遵循ARC内存管理方式,也就是说即使目前应用没有任何拥有该内存区域的对象了,这块内存区域也不会被释放,只有当应用周期结束(应用退出),该内存区域才会被释放。

所以说,加载图片的正确姿势一目了然,当加载需要重复使用的,体积比较小的图片,比如系统UI元素,用户头像等等,我们可以使用UIImage(named:)来初始化UIImage。如果像我们本文的需求呢,登录界面并不经常使用,不需要的时候直接从内存移除,就应该利用UIImage.init(contentsOfFile:)来初始化UIImage

关于UIImage(data:),是从NSData数据中加载图像,遵循ARC内存管理方式。

善后

我们在dismiss方法的completion闭包中设置了self.view = nil,这会导致再次进入该视图控制器的时候,会从loadView开始依次调用视图生命周期方法。

所以我们应该在viewDidLoad中逐个正常完成UI对象布局。

另外,特别要注意NSTimer这类对象的生命周期,该停掉的时候(比如在viewWillDisappear中)要注意invalidate

结果

/illustrations/2016-03-30-iOS-Singleton-ViewController-Performance-optimization/03.jpg

一张图说明问题,内存峰值的尾部,都是我dismiss掉视图控制器的时候,可见内存被立刻释放了。当我再次present该视图控制器时,又迎来一个波峰。

完整的演示项目:SKBFLoginViewController