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

本身作了一个模仿简书的小项目练手,主要布局是上面的scrollview有一排label,下面的scrollview有多个UITableView。点击上面的label,下面就能够显示不一样的页面。具体效果能够打开简书官方的APP查看,不少新闻软件也是这种效果。git

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

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

优化前内存

优化一:分离DataSource

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

// UIViewController.swift
var dataSource = ContentTableDatasource()

tableView.dataSource = dataSource
复制代码

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

extension ContentTableDatasource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //行数
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        //返回cell
    }
}
复制代码

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

优化二:重用ViewController

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

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

func scrollViewDidScroll(scrollView: UIScrollView) {
    // 加载即将出现的页面
    loadPage(page)
}

func loadPage(page: Int) {
    guard currentPage != page else { return }  //还在当前页面就不用加载了
    currentPage = page

    var pagesToLoad = [page - 1, page, page + 1]  // 筛选出须要加载的页面,通常只有一个
    var vcsToEnqueue: Array<ContentTableController> = []  // 把用不到的ViewController入队
}

func addViewControllerForPage(page: Int) {
    let vc = dequeueReusableViewController()  // 从队列中获取VC
    vc.pageID = page
    // 添加视图
}

func dequeueReusableViewController() -> ContentTableController {
    if reusableViewControllers.count > 0 {
        return reusableViewControllers.removeFirst() // 若是有能够重用的VC就直接返回
    }
    else { //不然就建立。程序刚开始运行的时候通常须要执行这一步
        let vc = ContentTableController()
        return vc
    }
}
复制代码

关于重用队列,能够参考这个项目:Reusespa

优化三:点击Label后的过渡

若是从第一页滑动到第三页,那么第二页也会快速闪过。这样会致使用户体验比较差。个人思路是首先在第二页的位置上覆盖一个和第一页如出一辙的UIView,而后不加动画的切换到第二页。这一瞬间用户感受不到任何变化。而后再有动画的滑动到第三页。滑动完成以后须要移除这个临时添加的UIView,关键步骤以下所示代理

var maskView = UIView()
maskView = bottomScrollViewController.currentDisplayViewController()?.view // 获取用于遮盖的view

bottomScrollView.addBottomViewAtIndex(targetPage - 1, view: maskView) // 把view添加到目标页的前一页
buttomScrollView.bottomScroll.setContentOffset(CGPointMake(previousOffSetX, 0), animated: false)  //无动画滑动
buttomScrollView.bottomScroll.setContentOffset(CGPointMake(offSetX, 0), animated: true) //有动画滑动

func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
    maskView.removeFromSuperview()  // 滑动结束后移除临时视图
}
复制代码

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

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

优化四:缓存DataSource

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

var dataSources: [Int: ContentTableDatasource] = [:]  // 键是页数,值是datasource对象

func bindDataSourceWithViewController(viewController: ContentTableController, page: Int) {
    if dataSources[page] == nil {  // 若是不存在,就去新建datasource
        dataSources[page] = ContentTableDatasource(page: page)
    }
    viewController.dataSource = dataSources[page]
}
复制代码

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

防掉坑指南

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

由于UIScrollView是能够滑动的,因此对它的布局约束要格外当心。举个例子,一个子视图的left已经肯定,这时候无论设置它的right约束仍是width约束均可以固定它的位置。可是在UIScrollView,千万不要设置right约束。不然你能够想象一下,有一个橡皮筋,一端被固定,另外一端被拉伸的感受:

make.right.equalTo(view) // 滑动时视图会被拉伸
make.width.equalTo(viewWidth) // 正确
复制代码

这样的bug很是难找到,因此我我的的经验是,在对UIScrollView的子视图布局时,尽可能不要用两端的位置来肯定视图本身的长度,而是应该经过本身长度肯定另外一端的位置。或者,干脆不要依赖于外部视图布局,而是用一个Container容器。这也是我在以前的文章中强烈推荐的方法。

成果:

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

优化后内存

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

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

相关文章
相关标签/搜索