本身作了一個模仿簡書的小項目練手,主要佈局是上面的scrollview有一排label,下面的scrollview有多個UITableView。點擊上面的label,下面就能夠顯示不一樣的頁面。具體效果能夠打開簡書官方的APP查看,不少新聞軟件也是這種效果。git
一開始的思路就是加載全部ViewController,由於是TableView,因此每一個TableView還有本身的DataSource,真機運行了一下,發現佔用內存大概是36M左右。因而我開始着手對這種原始的實現方案進行逐步優化,主要是內存佔用相關的,以及一些其餘的小技巧。github
項目在Github開源,本文涉及到的相關代碼均可以自行查看。項目地址:MJianshuswift
爲了輕量化UIViewController
,同時也爲了後期的解耦,我首先把DataSource
從UIViewController
中分離出來。思路是在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一文中。優化
若是不考慮點擊頂部標籤的狀況,也就是隻能滑動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
若是從第一頁滑動到第三頁,那麼第二頁也會快速閃過。這樣會致使用戶體驗比較差。個人思路是首先在第二頁的位置上覆蓋一個和第一頁如出一轍的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的切換和滑動切換頁面並非同一個原理,因此要保證他們之間的邏輯互不干擾
最初的邏輯是每一個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。