歡迎光臨
每天分享高質量文章

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

作者:青玉伏案的博客

鏈接:https://www.cnblogs.com/ludashi/p/5895725.html


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


關於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的初始化。


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



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



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


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



編號377,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

程式員求職面試

更多推薦25個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂