UIScrollView调优——节省超过50%内存

546 查看

自己做了一个模仿简书的小项目练手,主要布局是上面的scrollview有一排label,下面的scrollview有多个UITableView。点击上面的label,下面就可以显示不同的页面。具体效果可以打开简书官方的APP查看,很多新闻软件也是这种效果。

一开始的思路就是加载所有ViewController,因为是TableView,所以每个TableView还有自己的DataSource,真机运行了一下,发现占用内存大概是36M左右。于是我开始着手对这种原始的实现方案进行逐步优化,主要是内存占用相关的,以及一些其他的小技巧。

项目在Github开源,本文涉及到的相关代码都可以自行查看。项目地址:MJianshu

优化前内存

优化一:分离DataSource

为了轻量化UIViewController,同时也为了后期的解耦,我首先把DataSourceUIViewController中分离出来。思路是在UIViewController中引用一个DataSource对象,然后把table的dataSource属性设置成这个变量而不是自己,用代码描述就是:

把DataSource相关的代理方法都放到ContentTableDatasource中去:

这样做的好处在于,UIViewController对具体的数据获取一无所知,它只负责给table委派数据源的任务。只要改变数据源,table的内容就可以改变。这也符合MVC模式中M和C的解耦。更详细的介绍在objc.io的Lighter View Controllers一文中。

优化二:重用ViewController

如果不考虑点击顶部标签的情况,也就是只能滑动BottomScrollview,我们可以注意到一个事实。比如当前我在第五页,不管我要滑到其他的任何一页,都必须经过第四页或第六页。也就是说在这种情况下,除了4、5、6这三页的UIViewController,其他的都是无用的。一旦我向左滑到第四页,那么第六页的UIViewController也是无用的,它可以被重复利用,装载第三页所显示的UIView

所以,思路就是模仿UITableView的重用机制维护一个队列,实现UIViewController的重用。每当一个UIViewController变成无用的,就放入重用队列。需要UIViewController时先从重用队列中找,如果找不到就新建。这样一来内存中最多只会保存三个UIViewController的实例,所以占用内存大幅度降低。核心代码如下:

关于重用队列,可以参考这个项目:Reuse

优化三:点击Label后的过渡

如果从第一页滑动到第三页,那么第二页也会快速闪过。这样会导致用户体验比较差。我的思路是首先在第二页的位置上覆盖一个和第一页一模一样的UIView,然后不加动画的切换到第二页。这一瞬间用户感觉不到任何变化。然后再有动画的滑动到第三页。滑动完成之后需要移除这个临时添加的UIView,关键步骤如下所示

实际操作远比这个复杂。因为要实现UIViewController的重用,所以在scrollViewDidScroll这个代理方法中需要时刻监听滑动状态并加载下一页。在点击Label的时候需要禁掉这个特性。

总的来说,点击Label的切换和滑动切换页面并不是同一个原理,所以要保证他们之间的逻辑互不干扰

优化四:缓存DataSource

最初的逻辑是每个UIViewController自己处理自己的dataSource,现在因为在BottomScrollview中处理UIViewController的重用逻辑,所以dataSource的缓存和获取也就一并放在这里处理了。每个UIViewController重用时都会根据自己的页数去缓存中查找dataSource是否已经存在,如果已经存在的话就直接获取了。关键代码如下所示:

实际上dataSource也可以重用,但是这样做并不能节省太多内存,反而会导致dataSource中内容的反复切换,有点得不偿失

防掉坑指南

最后再谈一谈UIScrollView中的一些坑,之前也写过一篇文章——史上最简单的UIScrollView+Autolayout出坑指南,主要是关于UIScrollView在Autolayout下的布局问题。在后续的开发过程中,还是遇到了一些值得注意的地方。

因为UIScrollView是可以滑动的,所以对它的布局约束要格外小心。举个例子,一个子视图的left已经确定,这时候不管设置它的right约束还是width约束都可以固定它的位置。但是在UIScrollView,千万不要设置right约束。否则你可以想象一下,有一个橡皮筋,一端被固定,另一端被拉伸的感觉:

这样的bug非常难找到,所以我个人的经验是,在对UIScrollView的子视图布局时,尽量不要用两端的位置来确定视图自己的长度,而是应该通过自己长度确定另一端的位置。或者,干脆不要依赖于外部视图布局,而是用一个Container容器。这也是我在之前的文章中强烈推荐的方法。

成果:

内存占用显著减少,只有大约原来的一半。考虑到程序还有其他地方占用内存,可以认为重用机制降低了Scrollview超过50%的内存占用:

优化后内存

不过这么做还是稍有不足,如果数据量比较大,频繁的重用UIViewController会导致多次reloadData()。切换页面的时候会稍有卡顿的感觉。也许是我哪里考虑欠周,欢迎指正。目前来看,重用机智更适合于呈现静态内容的UIViewController

项目地址戳这里,欢迎star。