1. 程式人生 > >facebook新聞頁ListView優化

facebook新聞頁ListView優化

引言

原文連結:https://code.facebook.com/posts/879498888759525/fast-rendering-news-feed-on-android/ 透漏的資訊量不大,且大多數專案並不會遇到facebook這種ListView的場景,不過可以拓展下思路:邏輯單元不一定是檢視單元;移動端不要死搬MVC的架構,在市場上仍是中低端機型為主時,還是應該多考慮效能;附上rebbit的關於本文的討論,有些乾貨 :)

基礎知識

android系統每隔16.7ms發出一個渲染訊號,通知ui執行緒進行介面的渲染。為了達到流暢的體驗,應用程式需要在這個時間內完成應用邏輯, 使系統達到60fps。當一個Listview被新增到佈局時,其關聯的adapter的getView方法將會被回撥。在16.7毫秒這樣一個時間單元 內,可見listitem單元的getView方法將被按照順序執行。在大多數情況下,由於其他繪圖行為的存在,例如measure和 draw,getVIew實際分配到執行時間遠低於16ms。一旦listview包含複雜控制元件時,在16毫秒內不能完成渲染,使用者只能看到上一禎的結 果,這時就發生了掉幀。

Facebook新聞頁介紹

Facebook的新聞頁是一個複雜的listview控制元件,如何使它獲得流暢的滾動體驗一直困擾我們。 首先,新聞頁的每一條新聞的可見區域非常大,包含一系列的文字以及照片;其次,新聞的展現型別也很多樣,除了文字以及照片,新聞的附件還可包含連結、音 頻、視訊等。除此之外,新聞還可以被點贊、被轉載,導致一個新聞會被其他新聞包含在內。當新聞被大量使用者轉載時,甚至會出現一條新聞佔據兩個螢幕的情況。 加上android使用者的機型多為中低端裝置,這使我們在16.7ms內完成新聞頁的渲染變的非常困難。

新聞頁最初架構

在2012年,我們將新聞頁從web-view轉化成本地控制元件,在最初的那個版本中,基於View-Model-Binder設計模型,我們為新聞 listitem建立了一個自定義StoryView類,這個類有一個bindModel方法,該方法用於和資料進行繫結。程式碼是這樣的:

StoryView的包含的子控制元件都會有一個bindModel方法,例如HeadVIew通過該方法與其相關的資料進行繫結。

這種設計,程式碼非常直觀清晰,但他的缺點也很明顯:

  • listview複用機制不能有效的工作,Android's recycling mechanism does not work well in this case: Every item in the ListView was usually a StoryView, but once bound to a story, two StoryViews would be radically different and recycling one into the other wasn't effective.(這一段存疑,直接放原文)

  • 邏輯巢狀:採用bindModel繫結控制元件和資料,業務邏輯與檢視邏輯耦合,導致邏輯類層次非常深;

  • 佈局巢狀非常深:不但導致低效的檢視渲染,例如新聞被不停的轉載的極端場景下還會導致棧溢位;

  • bindModel方法邏輯過重:bindModel方法在當用戶滾動列表時被ui執行緒回撥,由於所有的資料解析都在這個方法內,導致該方法耗時

以上這些問題雖有他們單獨的解決方法,例如我們可以自己設計一套回收機制解決storyView複用問題。但基於維護成本和開發時間考慮,我們決定進行一次重構。

重構方案

重構工作大約是一年之前開始的,為了解決前一個架構的問題,首先我們決定將一條新聞分隔成多個listview item。例如,新聞的headerview將是一個獨立的listitem。這樣,我們可以利用android回收機制,HeaderView新聞子控 件將被不同的新聞複用。另外,切分成小view也使得記憶體佔用更小,在之前的架構中,Storyview部分的可見會導致這個Storyview被載入到 記憶體中,而現在,粒度更小,只有可見的子控制元件才會被載入。

另一個大的修改是,我們將檢視邏輯和資料邏輯分離,StoryView被分離成兩個類: 只負責展現的檢視類,以及一個Binder類。檢視類僅包含set方法(例如HeaderView包含了setTitle,setSubTitle。 setProfiePic等等)。Binder類包含了原來的bindMethod的邏輯,binder類包含三個方 法:prepare,bind,unbind。 bind方法呼叫view的set方法設定資料,unbind清理檢視資料,prepare方法在cpu空閒期間做一些預初始化工作,例如進行click 事件繫結、資料格式化、建立spannable等等,它會在getView方法之前被呼叫

我們遇到的技術難點是Binder的設計,由於StoryView被拆分不同的子控制元件,一條新聞可能會包含多個不同的Binder。而在之前,我們只需要根據檢視的樹結構進行結構化賦值。因此,我們引進了PartDefinition類,PartDefinition負責維護一條新聞包含哪些子控制元件、包含Binder的型別以及為新聞建立Binder類,有兩種型別的PartDefinition:單個PartDefinition以及PartDefinition集合。

一個新聞在重構之後的PartDefinition結構是這樣的:

結論

  • 採取新的架構,記憶體錯誤減少了17%,總crash率減少了8%,徹底解決漲溢位問題

  • 渲染時間減少了10%,大新聞場景不再掉幀

  • 精簡了原來的自定義回收機制,同時在重構過程中增加了單元測試