iOS開發之UITableview之多種Cell高度自適應實現方案的UI流暢度分析

本篇博客的主題是關於UI操作流暢度優化的一篇博客,我們以TableView中填充多個根據內容自適應高度的Cell來作爲本篇博客的使用場景。當然Cell高度的自適應網上的解決方案是鋪天蓋地呢,今天我們的重點不是如何討論Cell高度的自適應,而是給出幾種Cell高度自適應的解決方案,然後對比起UI流暢度,從而得出一些UI優化的一些常規做法。今天博客中主要用涉及的第三方庫是YYKitAsyncDisplayKit

關於YYKit和AsyncDisplayKit這兩個庫,本篇博客只是簡單的涉及到一些基本用法,主要是針對我們本篇博客的Demo來使用的,其中好多功能並未使用。因爲之前在項目中沒怎麼使用過這兩個框架,所以本篇博客就不着重介紹着兩個第三方框架了,如果你對其感興趣,Github上有你想要的內容,請自行搜索。廢話少說,進入今天的主題。

 

一、總述

本篇博客主要給出了5種Cell自適應高度的解決方案,並對比了每種實現方案的流暢度。也可以說是從UI最不流暢的一種我們慢慢優化,從而實現了這5種解決方案。當然我們是觀察屏幕的FPS來判斷屏幕在操作時是否卡頓。關於對FPS的實時監測,我參考了YYKit-Demo中的做法,並將其單獨提取了一個組件,便於我們項目的使用,關於這個提取的FPS組件,下方使用時會具體介紹。當然本篇博客所涉及的所有代碼,依然會分享到Github上,文章後方會給出相應的鏈接,有需要的小夥伴請自行clone。

下方這個截圖是我們今天demo的菜單列表頁面,點擊每個Cell都會跳轉左邊這個內容列表頁面。不過每個Cell所對應的內容頁面的Cell自適應高度的實現方式不同,我們在對其滑動操作時,可以根據下方這個FPS組件來觀察屏幕的流暢度。當然,每個內容列表頁的佈局和顯示內容都是相同的,不過不同的Cell自適應解決方案所對應的UI流暢度也是不同的。下面我們先大體的聊一下每種Cell自適應的實現方案。

  • Autolayout + AutomaticDimension: 該解決方案對應着,下方第一個Cell, 點擊該Cell進入的頁面完全由AutoLayout進行佈局,Cell自適應的高度也不用我們自己計算,而是使用系統提供的解決方案UITableViewAutomaticDimension來解決。當然,使用UITableViewAutomaticDimension要依賴於你添加的約束,稍後會介紹到。這種實現方案用起來簡單,不過UI流暢度方面不太理想。當TableView快速滑動時,就會出現掉幀,卡的不要不要的。
  • Autolayout + CountHeight: 這種解決方案依然是採用AutoLayout的方式來對Cell的內容進行佈局,不過Cell的高度我們是自己計算的,當然我們這個計算Cell高度的過程是放在子線程中進行的,所以這種實現方式要優於第一種實現方式,稍後會詳細介紹。
  • FrameLayout + CountHeight: 爲了進一步提高流暢度,我們採用了純Frame佈局,之前好像在哪兒看過,說Autolayout最終也是會被轉換成Frame進行佈局的,所以我們索性就使用Frame對整個Cell中的元素進行佈局。當然Cell高度已經Cell中可變內容的高度都是在子線程中進行計算的,這也是優化很重要的一步。這種實現方式還是比較流暢的,可以作爲折中的方案。
  • YYKit + CountHeight: 這種解決方案用到了YYKit中的控件,並且使用Frame佈局與Cell高度的計算。這種方式要由於上面的解決方案,比較YYKit中的一些控件做了優化。
  • AsyncDisplayKit + CountHeight: 則是使用了AsyncDisplayKit中提供的相關Note代替系統的原生控件,這種實現方式是這5種實現方式中最爲流暢的。稍後會詳細介紹。

上面這五種實現方式將是下方介紹的具體內容,當然會涉及一些其他的技術實現細節。

     

 

 

二、博客所涉及的自定義工具介紹

在進入主題之前,先進行預熱。先對本片博客中所涉及的一些小工具進行介紹。當然這些工具是自己封裝的,是本篇博客中所涉Demo的基礎,本部分將進行統一介紹,在使用時我們就一筆帶過即可。

 

1.工具一:FPSDisplay

上述Demo中使用到了一個小的組件是FPSDisplay, 用於實時顯示屏幕的刷新頻率的。我們知道現在iPhone的FPS是60。也就是每秒刷新60幀,如果低於60幀的話那就是掉幀了,如果掉幀掉的多的話就會明顯的看出卡頓。上述截圖中右下方的黑色圖標就是我們封裝的FPSDisplay工具。當然該工具是參考着YYKit-Demo中所實現的,對其進行的簡化和封裝,將其提取成了一個單獨的組件,便於在我們的應用中引入。

下方就是FPSDisplay引入並初始化的過程,下方是在AppDelegate中的didFinishLaunchingWithOptions中添加的。因爲FPSDisplay是添加在KeyWindow上的,所以在FPSDisplay初始化時要保證你的App已經有了KeyWindow了。進行下方初始化後,在你的App的右下方就會出現一個圖標來實時的顯示FPS。

  

 

FPSDisplay的實現並不麻煩,主要是CADisplayLink的使用,將創建CADisplayLink創建的對象添加到MainRunLoop中,就可以以此來計算FPS了。下方是FPSDisplay的核心代碼。在每次進行屏幕刷新時都會執行下方的tink方法,我們可以來計算1秒內刷新的次數,也就是所謂的FPS。代碼比較簡單,在此就不做過多的贅述了,詳細的代碼在Github上已經分享。

  

 

2.工具二:數據提供者

除了上述的FPSDislay工具外,我們還需要一個模塊,那就是爲Demo提供模擬數據的模塊。因爲我們沒有網絡模塊,我們就模擬網絡請求來生成數據,然後對數據進行處理生成Model。當然這個生成測試數據的過程沒有用到主線程,爲了不阻塞Main線程,我們需要將數據生成的部分在子線程中異步的執行。當然此處主要涉及多線程的東西。下方代碼段就是數據提供者DataSupport的核心代碼。

下方代碼段主要用到了並行隊列的異步執行任務組的使用,已經任務鎖的添加。下方首先創建了一個並行隊列concurrentQueue和隊列的任務組group,並且爲了數據同步,我們使用信號量創建了一個任務鎖lock。在for循環中我們異步的執行並行隊列來創建我們需要的數據模型Model。每循環一次創建一個Model,爲了Model數據的獨立性,在創建Model時,我們要爲其添加信號量同步鎖

當50條數據異步創建完畢後,我們需要將其提供給數據提供者的使用放,也就是在任務組中的任務都執行完畢後,會執行下方的notify方法。

  

在Model創建時,我們會對Model中可變的文字,也就是Cell中高度變化的內容的高度進行計算。當然該計算是在子線程中異步執行的。所以不會佔用主線程的時間來計算Cell的高度以及Cell中可變文字的高度。我們Model中有兩個字段就是來存儲Cell的高度以及可變文本的高度的,如下所示。這樣做的好處就是提高UI的流暢度。

  

 

3.工具三:UIImage對象的Memory緩存

第三個工具也是爲了提高數據流暢度而生的,就是圖片的對象緩存。我們將已經初始化過的圖片進行緩存,等下次再使用該圖片時直接從緩存中讀取,從而節省了在主線程中創建對象和銷燬對象的時間,從而可以提高UI的流暢度。當然此處我實現的圖片的內存緩存比較簡單,也就是在本Demo中適用。不過原理還是OK的,全面的MemoryCache請參考YYKit中的YYMemoryCache。其中用到了雙向鏈表以及CFMutableDictionaryRef來實現的MemoryCache,其源碼並不是很難理解,有興趣的小夥伴可以進行閱讀呢。

本篇博客所實現的Memory緩存就比較簡單了,就使用了一個字典,字典的Key是圖片的名稱,字典的Value是已經創建的字典的對象。代碼比較簡單,下方是核心代碼。大體原理就是在獲取時,如果緩存字典中沒有相應的對象就進行創建並加入緩存,然後返回該對象。如果緩存中已經有該對象,則直接返回。核心代碼如下。

  

 

 

三、Autolayout + AutomaticDimension

上一部分已經爲Demo的開發做好了準備,接下來就開始進入今天真正的主題。首先我們來介紹Autolayout + AutomaticDimension的實現方式。使用這種方式來是Cell高度的自適應比較簡單,但不高效。下方是我們所使用的Cell的佈局,當然是使用AutoLayout來實現的。因爲下方test的內容的長度是不定的,所以我們爲test所對應的TextView添加的約束爲(top, left right, bottom)。這樣test的高度就可以隨着Cell的高度而改變了。

    

約束添加完畢後,我們的工作基本上就已經完成了,接下來需要進行簡單的配置,我們的Cell高度自適應就OK了。下方就是我們添加完約束後要做的事情,需要給我們的tableView設置一個預估值(estimatedRowHeight), 然後在TableViewDelegate的heightForRowAtIndexPath方法中返回UITableViewAutomaticDimension該屬性即可。這樣Cell就可以根據可變的文字高度來自適應了。當然該方法在iOS8以上的系統上纔可以使用。

  

 

經過上述這兩步,我們的Cell就可以進行自適應了,下方是該解決方案所對應的運行效果。可以看出來卡頓還是比較明顯的,掉幀比較嚴重,在Cell高度自適應時最好不要採用此方法。也就是說這種方法,並不適用在我們Cell列表中來預估每個Cell的高度。那這種方式是不是就沒用了呢?當然不是,填寫內容的Cell上是可以使用這種方法進行預估的,也就是說,當根據用戶輸入的內容來實時改變Cell的高度,是可以使用該方法的。

  

 

 

四、Autolayout +CountHeight

接下來我們對上述的效果進行優化,不使用TableView的預估值了,而是直接使用我們在子線程中計算的文本高度。當然依然是使用AutoLayout的方式,將上述返回高度的方法heightForRowAtIndexPath中的內容進行替換,直接返回當前Model中Cell的高度,如下所示:

  

 

經過上面這麼一修改,我們就可以將之前Cell高度計算的內容移到子線程中了,上述的卡頓問題會得到些微的解決。下方是該方式的運行效果,可以看出來比上述的實現方式稍微好一些,不過還是有些掉幀,掉幀也是比較嚴重的。

  

 

 

五、FrameLayout + CountHeight

上述結果仍然不理想,我們接着優化。我們不使用AutoLayout佈局,我們直接使用Frame來佈局,這樣就減少了由AutoLayout轉換到FrameLayout的時間。本部分我們就使用純代碼的方式,以Autolayout進行佈局。在給Cell配置數據的時候我們根據Model中計算的高度來修改可變文字內容的高度,如下所示:

  

下方是使用這種方式最終的運行效果,從該效果中可以看出,效果還是蠻OK的。雖然有些掉幀,但是還是非常流暢的,這種流暢度是可以接受的。如果你不想使用第三方庫的話,這種方式還是一個比較好的解決方案的。

  

 

 

六. YYKit + CountHeight

接下來我們進一步進行優化,引入第三方UI組件YYKit。將Cell上的組件替換成YYKit所提供的組件。然後使用Frame進行佈局,當然也是在子線程中對Cell的高度進行計算了。當然此處只是對YYKit簡單的使用,應該還有更好的優化方式,只是此處沒有給出,歡迎相互交流。

  

看來將進行系統的基礎控件換成了YYKit中的控件,下方是此解決方案的運行效果。單從效果上來看,還是比較流暢的,但是爲達到完全不掉幀的效果。不過整體看來還是比較流暢的。

  

 

 

七、AsyncDisplayKit + CountHeight

接下來我們要用Facebook提供的第三方庫來進行基礎組件的替換,將我們使用到的組件替換成AsyncDisplayKit相應的Note,如下所示。這些Note是對系統組件的重組,對組件的顯示進行了優化,讓其渲染更爲流暢。

  

下方就是使用AsyncDisplayKit重構後運行的效果。從下方的效果上來看,幾乎不掉幀,那個流暢呢。如果你對UI流暢度要求比較高的話,那麼AsyncDisplayKit是一個比較好的選擇。不過會嚴重依賴AsyncDisplayKit,如果AsyncDisplayKit停止維護了,後期對AsyncDisplayKit進行替換的話,工作量還是比較大的。因爲這種佈局框架不像網絡框架,我們可以對網絡框架的調用進行提取,網絡層統一對外接口,很方便切換到其他網絡請求庫。但是像AsyncDisplayKit這種框架會散佈於UI層的各個角落,封裝提取不易,更不用說輕而易舉的替換了。所以像這種頁面的實現,個人還是偏向於Framelayout + CountHeight的方式來實現。

  

 

 

八、Demo中用到的設計模式

經過上面這7步,我們Demo的功能以及效果已經介紹完畢,不同實現方式優缺點一目瞭然。該部分也是本篇博客最後一部分,我們就來聊一下本篇博客中所使用的設計模式。我們可以看出上述幾個列表的頁面是完全一樣的,只是Cell的實現方式不同。所以我們可以將TableView提取成基類,TableView中所使用的Cell類型由子類來確定。說的官方一些,這就是策略模式。具體的Cell使用策略由具體的TableView來定,而父類TableView值負責根據子類提供的策略來進行Cell的初始化。

我們就以AsyncDisplayKitTableViewControllerFrameCountTableViewController這兩個類爲例,下方就是這兩個TableViewController的相關代碼。下方這兩個類的基類都是SuperTableViewController。大部分工作都在基類中去實現了,而子類中只提供了使用Cell的策略。這就是策略模式的好處,便於擴充,如果有類似的頁面,子類只提供Cell的類型即可。下方這兩個類中的getReuseIdentifier方法就是爲父類提供策略的方法。

   

當然不知上述類有父類,具體Cell的基類也得有父類,因爲在TableViewController中聲明Cell時用的是Cell的父類,如下所示。此處用到了面向對象的多態性,並且也用到了面向接口原則。此處SuperTableViewCell雖然是一個基類,但是它也擔負着定義子類接口的責任。好處就不多說了吧。  

 

關於設計模式相關的內容,請查看之前發佈的關於設計模式的系列博客設計模式系列,重構的內容的話請查看之前發佈重構系列的博客《重構系列》。當然這兩個系列的博客全是使用Swift語言實現的Demo,不過思想都是相同的。好了今天博客篇幅也挺長的,就先到這兒吧。

github分享鏈接:https://github.com/lizelu/DisplayTestDemo

 

作者:青玉伏案 
出處:http://www.cnblogs.com/ludashi/ 
本文版權歸作者和共博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。 
如果文中有什麼錯誤,歡迎指出。以免更多的人被誤導。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章