AsyncDisplayKit介紹(三)深度優化列表效能

即刻技術團隊發表於2017-09-22

(ASDK已改名Texture)

說到檢視效能,不能不提到UITableView,對於它的滾動效能的討論和優化從未停止。在我們的探索過程中,嘗試過以下一些措施:

  • Cell reuse,Apple原生支援
  • estimated cell height,iOS8開始原生支援
  • 手動將計算完成的height快取(或使用FDTemplateLayoutCell等框架自動計算)
  • prefetch API,iOS10開始原生支援
  • 非同步載入cell內容,文字圖片等

還有一些諸如圓角、opaque等普通UIView可能遇到的效能瓶頸已經在第一篇中討論了一些,這裡不再贅述。

然而我們會想,cell的非同步佈局、圖片和文字渲染是否還可以優化,預載入是否可以更完善更智慧?仍然有許多問題等待被解決。

UITableView載入Cell的過程

我們先看一下一般UITableView載入Cell的過程:

  1. cellForRowAtIndexPath,讀取model
  2. 從reuse池中dequeue一個cell,呼叫prepareForReuse重置其狀態
  3. 將model裝配到UITableViewCell當中去
  4. 佈局(耗時且無法快取的Autolayout),渲染(文字和圖片都很耗時),顯示

這些操作都在cell將要進入window的一剎那發生,不難理解,在短短的16ms裡(60fps)是很難完成這些任務的,尤其是當使用者快速滾動的時候,大量任務堆積在runloop,情況變得雪上加霜。

如果將滾動中的CPU佔用情況用圖表顯示出來,大概是這樣的(WWDC16 session 219):

這其中每當一個cell將要進入螢幕,一個尖峰就會產生。而在其他相對空閒的時候,cpu的負載相當的低。

自然我們會想到,如果把任務平均分配到一個時間段內,而不是集中在某一個點,是否就可以避免這樣的情況發生?如果我們能夠預測一個cell很快將要進入螢幕,而此時cpu空閒,是否可以未雨綢繆,提前做一些佈局和渲染的工作?那樣一來,在cell真正需要顯示的時候,由於佈局和渲染結果backing store已經是現成的了,只需要將它送給真正負責顯示的view就可以,也就可以避免產生劇烈的效能波動。

ASTableNode/ASCollectionNode開闢的新航路

首先的好訊息是,作為ASDK的一員,ASTableNode以及其cellNode已經具備了非同步佈局和非同步渲染的能力,即使沒有做額外優化,僅僅利用ASDK通用的非同步機制將耗時操作延後,相對於一般UITableView已經有了顯著的提升。雖然效能鋸齒仍然存在,但是將其轉移到了後臺執行緒以後,使用者感受到的卡頓就已經不會那麼明顯了。

然而這些似乎還不夠,在進入螢幕之後才開始渲染,會有短暫的白屏現象(等待渲染完成)再顯示內容。既然渲染工作可以在顯示之後再進行,那麼類似的,也可以在顯示之前的一段時間,把佈局和渲染的工作預先完成。

要達到這些目的,首先介紹一些相關的類:

  • ASTableNode/ASCollectionNode,可以認為是UITableView/UICollectionView的非同步版本,內部包裝了原來的UIKit的對應版本,並擴充套件了一系列功能使他們能夠實現非同步佈局及渲染。
  • ASInterfaceState,表示一個node不同的顯示狀態。其實每個ASDisplayNode都具備interfaceState屬性,它主要的用武之地還是在tableNode/collectionNode之中。對於一個UITableViewCell來說,佈局和渲染一般都是在cellForRowAtIndexpath同時完成,然而當需要精細處理任務時就需要把每一個不同的狀態分開,降低某一瞬間由於CPU負載高導致卡頓的可能性。ASInterfaceState遞進地分為5種狀態:
    • None,該node在一段時間內不會進入螢幕
    • MeasureLayout,可能會在一段時間後進入螢幕,應該準備layout和size計算
    • Preload,載入所需要的資料,如下載圖片,快取讀取等等
    • Display,馬上將要進入螢幕,開始進行渲染操作,顯示包含的文字或者圖片
    • Visible,該node(所對應的view)至少有1個畫素已經在螢幕內,正在顯示

對於每一個cell而言,原本需要在同一時間點進行的所有初始化/載入/佈局/渲染等工作,現在被均勻分配到了不同的狀態進行預處理。隨著使用者滾動列表,根據cell離螢幕的距離不同,設定相應的interfaceState並觸發不同階段的工作,達到均勻分配的目的。同時,由於不需要在主執行緒上進行,多個cell的工作可以通過共享後臺執行緒來大幅提高並行效率。

  • ASDataController,與ASTableNode一一對應,負責代替ASTableNode管理delegate和dataSource的一系列方法,諸如初始化,插入,刪除和一些代理方法等。
  • ASRangeController,同樣與ASTableNode一一對應,並且可以根據裝置效能自定義佈局、載入、渲染的工作indexPath區間,在滾動時動態高效地調整各cell的interfaceState來層層觸發不同顯示階段的工作,對於流暢滾動起到了至關重要的作用。

  • ASScrollDirection,定義了列表滾動的方向(上下左右)。在ASRangeController調整各階段的工作區間時,一般在使用者滾動的方向上需要多載入一些,而滾出螢幕的cell在一定時間內回到螢幕的概率較低,因此其分配到的資源也就相應少一些。

在ASDK1.x的時代,由於彼時還沒有ASRangeController的存在,cell的渲染只會在進入螢幕以後進行,也就是說,雖然效能能夠達到60fps,但是滾動較快時,渲染跟不上,『白屏』現象就出現了。到了2.x有了ASRangeController之後,雖然在滾動極快的情況下仍然會因為資源不足而產生白屏現象,但是在一般情況下,因為資源分配更加合理,這個問題得到了顯著的改善。

一些細節

多執行緒

當同時layout多個node時,如何均勻分配工作到各個執行緒,同時單次不佔用過多cpu時間?

ASDK是這麼做的:

獲取當前裝置上cpu的數量,並乘以每個cpu的工作量,如4 * 5 = 20,即同一批最多對20個node進行佈局。(儘管沒有找到嚴格的文件來說明這樣的計算方式會帶來最高的效率,但是應該要比不分批次處理更優,使佔用的cpu時間片可控)
呼叫dispatch_apply,對同一批次20個node進行並行佈局計算
每一批處理20個,直到所有都處理完

監聽狀態變化

不光ASDisplayNode本身會根據不同的state進行相應的工作,它同時也提供了一系列的方法供子類override,如didEnterPreloadState/didExitVisibleState等等。在實際應用中,由於子類通常會持有一些自己管理的資源(如圖片),需要控制在顯示/離開螢幕之際進行資源的分配/回收。由於每個時間點分的比較細,只要將工作均勻、合理地分配到相應的方法中,就可以實現非常精確高效的資源排程。

記憶體管理

ASRangeController經常需要管理螢幕外的node(可能同時有好幾屏的內容同時進行佈局計算和顯示),通過預處理來減輕將來的工作量,是一個典型的『空間換時間』的辦法,對於記憶體的壓力自然就會上升。

為此,ASRangeController提供了一些引數,讓開發者可以自行決定每個stage所囊括的範圍,達到控制記憶體的:

ASLayoutRangeModeFull,此時用到的資源較多,同時使用者體驗也是最好的
ASLayoutRangeMinimum,比上一種型別節省一些資源
ASLayoutRangeModeVisibleOnly,在app退到後臺的時候自動設定,將螢幕外的node所佔用的資源釋放,降低app在系統中被殺的概率
ASLayoutRangeModeLowMemory,比上一種更省記憶體,對於app退到後臺,並且目前沒有在螢幕上(可能在navigation stack裡)的rangeController適用,最大程度釋放資源
我們可以通過呼叫setTuningParameters的方式,對於每一種mode中的每一種layoutRangeType做出精細調整。同時,ASDK會自動根據此時node在螢幕上的情況,自動在以上幾種mode中來回切換,並根據指定的引數範圍來載入/釋放資源,達到資源和效能的平衡。

ASDK中還自帶了一個顯示rangeController工作情況的小工具:

即刻在熱門頁面有4個tab,相應的右下角的debug view顯示的分別是4個頁面不同的working range。箭頭表示當前的滾動方向,由於我們配置了在滾動方向上載入的距離比反方向要更長,因此可以看到在滾動方向上的色塊會更長一些。

即刻的實踐

即刻iOS在最初時也曾苦苦找尋列表效能優化方案。在閱讀了Ray Wenderlich的一篇文章之後,驚豔其出色的效能,開始接觸ASDK。最開始嘗試在訊息頁面使用,後來在長期實踐中,發現其確實能夠解決tableView的行高計算和效能問題,才漸漸在其他頁面使用。

就如同在本系列第一篇提到的,對於先進的框架雖然不能重度依賴,我們也願意勇於擁抱其先進之處。站在ASDK的角度看UITableView,就能有更大的空間來重新審視列表滾動的本質,將效能和資源分配效率提升到一個新的高度;即使將來脫離ASDK,仍然有一些想法值得我們進一步思考。

想體驗即刻的朋友可以下載看一下,最近又增加了不少新玩法 :)

PS: 我們在招Android、爬蟲、後端的職位,連結:jike.ruguoapp.com/careers

相關文章