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。

相關文章
相關標籤/搜索