UIScrollView調優——節省超過50%記憶體

發表於2016-03-04

自己做了一個模仿簡書的小專案練手,主要佈局是上面的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。

相關文章