一玄括、總述
本篇博客主要給出了5種Cell自適應(yīng)高度的解決方案分瘦,并對比了每種實現(xiàn)方案的流暢度拇砰。也可以說是從UI最不流暢的一種我們慢慢優(yōu)化,從而實現(xiàn)了這5種解決方案匕垫。當(dāng)然我們是觀察屏幕的FPS來判斷屏幕在操作時是否卡頓僧鲁。關(guān)于對FPS的實時監(jiān)測,我參考了YYKit-Demo中的做法象泵,并將其單獨(dú)提取了一個組件寞秃,便于我們項目的使用,關(guān)于這個提取的FPS組件偶惠,下方使用時會具體介紹春寿。當(dāng)然本篇博客所涉及的所有代碼,依然會分享到Github上忽孽,文章后方會給出相應(yīng)的鏈接绑改,有需要的小伙伴請自行clone。
下方這個截圖是我們今天demo的菜單列表頁面兄一,點擊每個Cell都會跳轉(zhuǎn)左邊這個內(nèi)容列表頁面厘线。不過每個Cell所對應(yīng)的內(nèi)容頁面的Cell自適應(yīng)高度的實現(xiàn)方式不同,我們在對其滑動操作時出革,可以根據(jù)下方這個FPS組件來觀察屏幕的流暢度造壮。當(dāng)然,每個內(nèi)容列表頁的布局和顯示內(nèi)容都是相同的骂束,不過不同的Cell自適應(yīng)解決方案所對應(yīng)的UI流暢度也是不同的耳璧。下面我們先大體的聊一下每種Cell自適應(yīng)的實現(xiàn)方案。
Autolayout + AutomaticDimension:該解決方案對應(yīng)著展箱,下方第一個Cell, 點擊該Cell進(jìn)入的頁面完全由AutoLayout進(jìn)行布局旨枯,Cell自適應(yīng)的高度也不用我們自己計算,而是使用系統(tǒng)提供的解決方案UITableViewAutomaticDimension來解決析藕。當(dāng)然召廷,使用UITableViewAutomaticDimension要依賴于你添加的約束凳厢,稍后會介紹到。這種實現(xiàn)方案用起來簡單竞慢,不過UI流暢度方面不太理想先紫。當(dāng)TableView快速滑動時,就會出現(xiàn)掉幀筹煮,卡的不要不要的遮精。
Autolayout + CountHeight:這種解決方案依然是采用AutoLayout的方式來對Cell的內(nèi)容進(jìn)行布局,不過Cell的高度我們是自己計算的败潦,當(dāng)然我們這個計算Cell高度的過程是放在子線程中進(jìn)行的本冲,所以這種實現(xiàn)方式要優(yōu)于第一種實現(xiàn)方式,稍后會詳細(xì)介紹劫扒。
FrameLayout + CountHeight:為了進(jìn)一步提高流暢度檬洞,我們采用了純Frame布局,之前好像在哪兒看過沟饥,說Autolayout最終也是會被轉(zhuǎn)換成Frame進(jìn)行布局的添怔,所以我們索性就使用Frame對整個Cell中的元素進(jìn)行布局。當(dāng)然Cell高度已經(jīng)Cell中可變內(nèi)容的高度都是在子線程中進(jìn)行計算的贤旷,這也是優(yōu)化很重要的一步广料。這種實現(xiàn)方式還是比較流暢的,可以作為折中的方案幼驶。
YYKit + CountHeight:這種解決方案用到了YYKit中的控件艾杏,并且使用Frame布局與Cell高度的計算。這種方式要由于上面的解決方案盅藻,比較YYKit中的一些控件做了優(yōu)化购桑。
AsyncDisplayKit + CountHeight:則是使用了AsyncDisplayKit中提供的相關(guān)Note代替系統(tǒng)的原生控件,這種實現(xiàn)方式是這5種實現(xiàn)方式中最為流暢的萧求。稍后會詳細(xì)介紹其兴。
上面這五種實現(xiàn)方式將是下方介紹的具體內(nèi)容,當(dāng)然會涉及一些其他的技術(shù)實現(xiàn)細(xì)節(jié)夸政。
二、博客所涉及的自定義工具介紹
在進(jìn)入主題之前榴徐,先進(jìn)行預(yù)熱守问。先對本片博客中所涉及的一些小工具進(jìn)行介紹。當(dāng)然這些工具是自己封裝的坑资,是本篇博客中所涉Demo的基礎(chǔ)耗帕,本部分將進(jìn)行統(tǒng)一介紹,在使用時我們就一筆帶過即可袱贮。
1.工具一:FPSDisplay
上述Demo中使用到了一個小的組件是FPSDisplay, 用于實時顯示屏幕的刷新頻率的仿便。我們知道現(xiàn)在iPhone的FPS是60。也就是每秒刷新60幀,如果低于60幀的話那就是掉幀了嗽仪,如果掉幀掉的多的話就會明顯的看出卡頓荒勇。上述截圖中右下方的黑色圖標(biāo)就是我們封裝的FPSDisplay工具。當(dāng)然該工具是參考著YYKit-Demo中所實現(xiàn)的闻坚,對其進(jìn)行的簡化和封裝沽翔,將其提取成了一個單獨(dú)的組件,便于在我們的應(yīng)用中引入窿凤。
下方就是FPSDisplay引入并初始化的過程仅偎,下方是在AppDelegate中的didFinishLaunchingWithOptions中添加的。因為FPSDisplay是添加在KeyWindow上的雳殊,所以在FPSDisplay初始化時要保證你的App已經(jīng)有了KeyWindow了橘沥。進(jìn)行下方初始化后,在你的App的右下方就會出現(xiàn)一個圖標(biāo)來實時的顯示FPS夯秃。
FPSDisplay的實現(xiàn)并不麻煩座咆,主要是CADisplayLink的使用,將創(chuàng)建CADisplayLink創(chuàng)建的對象添加到MainRunLoop中寝并,就可以以此來計算FPS了箫措。下方是FPSDisplay的核心代碼。在每次進(jìn)行屏幕刷新時都會執(zhí)行下方的tink方法衬潦,我們可以來計算1秒內(nèi)刷新的次數(shù)斤蔓,也就是所謂的FPS。代碼比較簡單镀岛,在此就不做過多的贅述了弦牡,詳細(xì)的代碼在Github上已經(jīng)分享。
2.工具二:數(shù)據(jù)提供者
除了上述的FPSDislay工具外漂羊,我們還需要一個模塊驾锰,那就是為Demo提供模擬數(shù)據(jù)的模塊。因為我們沒有網(wǎng)絡(luò)模塊走越,我們就模擬網(wǎng)絡(luò)請求來生成數(shù)據(jù)椭豫,然后對數(shù)據(jù)進(jìn)行處理生成Model。當(dāng)然這個生成測試數(shù)據(jù)的過程沒有用到主線程旨指,為了不阻塞Main線程赏酥,我們需要將數(shù)據(jù)生成的部分在子線程中異步的執(zhí)行。當(dāng)然此處主要涉及多線程的東西谆构。下方代碼段就是數(shù)據(jù)提供者DataSupport的核心代碼裸扶。
下方代碼段主要用到了并行隊列的異步執(zhí)行,任務(wù)組的使用搬素,已經(jīng)任務(wù)鎖的添加呵晨。下方首先創(chuàng)建了一個并行隊列concurrentQueue和隊列的任務(wù)組group魏保,并且為了數(shù)據(jù)同步,我們使用信號量創(chuàng)建了一個任務(wù)鎖lock摸屠。在for循環(huán)中我們異步的執(zhí)行并行隊列來創(chuàng)建我們需要的數(shù)據(jù)模型Model谓罗。每循環(huán)一次創(chuàng)建一個Model,為了Model數(shù)據(jù)的獨(dú)立性餐塘,在創(chuàng)建Model時妥衣,我們要為其添加信號量同步鎖。
當(dāng)50條數(shù)據(jù)異步創(chuàng)建完畢后戒傻,我們需要將其提供給數(shù)據(jù)提供者的使用放税手,也就是在任務(wù)組中的任務(wù)都執(zhí)行完畢后,會執(zhí)行下方的notify方法需纳。
在Model創(chuàng)建時芦倒,我們會對Model中可變的文字,也就是Cell中高度變化的內(nèi)容的高度進(jìn)行計算不翩。當(dāng)然該計算是在子線程中異步執(zhí)行的兵扬。所以不會占用主線程的時間來計算Cell的高度以及Cell中可變文字的高度。我們Model中有兩個字段就是來存儲Cell的高度以及可變文本的高度的口蝠,如下所示器钟。這樣做的好處就是提高UI的流暢度。
3.工具三:UIImage對象的Memory緩存
第三個工具也是為了提高數(shù)據(jù)流暢度而生的妙蔗,就是圖片的對象緩存傲霸。我們將已經(jīng)初始化過的圖片進(jìn)行緩存,等下次再使用該圖片時直接從緩存中讀取眉反,從而節(jié)省了在主線程中創(chuàng)建對象和銷毀對象的時間昙啄,從而可以提高UI的流暢度。當(dāng)然此處我實現(xiàn)的圖片的內(nèi)存緩存比較簡單寸五,也就是在本Demo中適用梳凛。不過原理還是OK的,全面的MemoryCache請參考YYKit中的YYMemoryCache梳杏。其中用到了雙向鏈表以及CFMutableDictionaryRef來實現(xiàn)的MemoryCache韧拒,其源碼并不是很難理解,有興趣的小伙伴可以進(jìn)行閱讀呢十性。
本篇博客所實現(xiàn)的Memory緩存就比較簡單了叭莫,就使用了一個字典,字典的Key是圖片的名稱烁试,字典的Value是已經(jīng)創(chuàng)建的字典的對象。代碼比較簡單拢肆,下方是核心代碼减响。大體原理就是在獲取時靖诗,如果緩存字典中沒有相應(yīng)的對象就進(jìn)行創(chuàng)建并加入緩存,然后返回該對象支示。如果緩存中已經(jīng)有該對象掖鱼,則直接返回荚醒。核心代碼如下。
三、Autolayout + AutomaticDimension
上一部分已經(jīng)為Demo的開發(fā)做好了準(zhǔn)備罢杉,接下來就開始進(jìn)入今天真正的主題。首先我們來介紹Autolayout + AutomaticDimension的實現(xiàn)方式看峻。使用這種方式來是Cell高度的自適應(yīng)比較簡單房轿,但不高效。下方是我們所使用的Cell的布局栽渴,當(dāng)然是使用AutoLayout來實現(xiàn)的尖坤。因為下方test的內(nèi)容的長度是不定的,所以我們?yōu)閠est所對應(yīng)的TextView添加的約束為(top, left right, bottom)闲擦。這樣test的高度就可以隨著Cell的高度而改變了慢味。
約束添加完畢后,我們的工作基本上就已經(jīng)完成了墅冷,接下來需要進(jìn)行簡單的配置纯路,我們的Cell高度自適應(yīng)就OK了。下方就是我們添加完約束后要做的事情寞忿,需要給我們的tableView設(shè)置一個預(yù)估值(estimatedRowHeight), 然后在TableViewDelegate的heightForRowAtIndexPath方法中返回UITableViewAutomaticDimension該屬性即可驰唬。這樣Cell就可以根據(jù)可變的文字高度來自適應(yīng)了。當(dāng)然該方法在iOS8以上的系統(tǒng)上才可以使用罐脊。
經(jīng)過上述這兩步定嗓,我們的Cell就可以進(jìn)行自適應(yīng)了,下方是該解決方案所對應(yīng)的運(yùn)行效果萍桌∠Γ可以看出來卡頓還是比較明顯的,掉幀比較嚴(yán)重上炎,在Cell高度自適應(yīng)時最好不要采用此方法恃逻。也就是說這種方法,并不適用在我們Cell列表中來預(yù)估每個Cell的高度藕施。那這種方式是不是就沒用了呢寇损?當(dāng)然不是,填寫內(nèi)容的Cell上是可以使用這種方法進(jìn)行預(yù)估的裳食,也就是說矛市,當(dāng)根據(jù)用戶輸入的內(nèi)容來實時改變Cell的高度,是可以使用該方法的诲祸。
四浊吏、Autolayout +CountHeight
接下來我們對上述的效果進(jìn)行優(yōu)化而昨,不使用TableView的預(yù)估值了,而是直接使用我們在子線程中計算的文本高度找田。當(dāng)然依然是使用AutoLayout的方式歌憨,將上述返回高度的方法heightForRowAtIndexPath中的內(nèi)容進(jìn)行替換,直接返回當(dāng)前Model中Cell的高度墩衙,如下所示:
經(jīng)過上面這么一修改务嫡,我們就可以將之前Cell高度計算的內(nèi)容移到子線程中了,上述的卡頓問題會得到些微的解決漆改。下方是該方式的運(yùn)行效果心铃,可以看出來比上述的實現(xiàn)方式稍微好一些,不過還是有些掉幀于个,掉幀也是比較嚴(yán)重的暮顺。
五、FrameLayout + CountHeight
上述結(jié)果仍然不理想捶码,我們接著優(yōu)化羽氮。我們不使用AutoLayout布局惫恼,我們直接使用Frame來布局,這樣就減少了由AutoLayout轉(zhuǎn)換到FrameLayout的時間祈纯。本部分我們就使用純代碼的方式令宿,以Autolayout進(jìn)行布局。在給Cell配置數(shù)據(jù)的時候我們根據(jù)Model中計算的高度來修改可變文字內(nèi)容的高度腕窥,如下所示:
下方是使用這種方式最終的運(yùn)行效果簇爆,從該效果中可以看出,效果還是蠻OK的响蓉。雖然有些掉幀哨毁,但是還是非常流暢的,這種流暢度是可以接受的言秸。如果你不想使用第三方庫的話,這種方式還是一個比較好的解決方案的。
六. YYKit + CountHeight
接下來我們進(jìn)一步進(jìn)行優(yōu)化抄沮,引入第三方UI組件YYKit岖瑰。將Cell上的組件替換成YYKit所提供的組件。然后使用Frame進(jìn)行布局率挣,當(dāng)然也是在子線程中對Cell的高度進(jìn)行計算了露戒。當(dāng)然此處只是對YYKit簡單的使用,應(yīng)該還有更好的優(yōu)化方式智什,只是此處沒有給出荠锭,歡迎相互交流。
看來將進(jìn)行系統(tǒng)的基礎(chǔ)控件換成了YYKit中的控件删豺,下方是此解決方案的運(yùn)行效果愧怜。單從效果上來看,還是比較流暢的赔桌,但是為達(dá)到完全不掉幀的效果渴逻。不過整體看來還是比較流暢的。
七雪位、AsyncDisplayKit + CountHeight
接下來我們要用Facebook提供的第三方庫來進(jìn)行基礎(chǔ)組件的替換雹洗,將我們使用到的組件替換成AsyncDisplayKit相應(yīng)的Note,如下所示庇茫。這些Note是對系統(tǒng)組件的重組螃成,對組件的顯示進(jìn)行了優(yōu)化,讓其渲染更為流暢宁炫。
下方就是使用AsyncDisplayKit重構(gòu)后運(yùn)行的效果羔巢。從下方的效果上來看罩阵,幾乎不掉幀,那個流暢呢袍辞。如果你對UI流暢度要求比較高的話常摧,那么AsyncDisplayKit是一個比較好的選擇。不過會嚴(yán)重依賴AsyncDisplayKit谎懦,如果AsyncDisplayKit停止維護(hù)了溃斋,后期對AsyncDisplayKit進(jìn)行替換的話梗劫,工作量還是比較大的。因為這種布局框架不像網(wǎng)絡(luò)框架蛉威,我們可以對網(wǎng)絡(luò)框架的調(diào)用進(jìn)行提取走哺,網(wǎng)絡(luò)層統(tǒng)一對外接口,很方便切換到其他網(wǎng)絡(luò)請求庫择示。但是像AsyncDisplayKit這種框架會散布于UI層的各個角落,封裝提取不易汪诉,更不用說輕而易舉的替換了剪菱。所以像這種頁面的實現(xiàn)孝常,個人還是偏向于Framelayout + CountHeight的方式來實現(xiàn)蚓哩。
八岸梨、Demo中用到的設(shè)計模式
經(jīng)過上面這7步,我們Demo的功能以及效果已經(jīng)介紹完畢半开,不同實現(xiàn)方式優(yōu)缺點一目了然赃份。該部分也是本篇博客最后一部分,我們就來聊一下本篇博客中所使用的設(shè)計模式纠永。我們可以看出上述幾個列表的頁面是完全一樣的谒拴,只是Cell的實現(xiàn)方式不同英上。所以我們可以將TableView提取成基類,TableView中所使用的Cell類型由子類來確定惭聂。說的官方一些易遣,這就是策略模式。具體的Cell使用策略由具體的TableView來定侨歉,而父類TableView值負(fù)責(zé)根據(jù)子類提供的策略來進(jìn)行Cell的初始化。
我們就以AsyncDisplayKitTableViewController和FrameCountTableViewController這兩個類為例炮温,下方就是這兩個TableViewController的相關(guān)代碼牵舵。下方這兩個類的基類都是SuperTableViewController畸颅。大部分工作都在基類中去實現(xiàn)了,而子類中只提供了使用Cell的策略涛癌。這就是策略模式的好處送火,便于擴(kuò)充种吸,如果有類似的頁面,子類只提供Cell的類型即可坚俗。下方這兩個類中的getReuseIdentifier方法就是為父類提供策略的方法坦冠。
當(dāng)然不知上述類有父類辙浑,具體Cell的基類也得有父類,因為在TableViewController中聲明Cell時用的是Cell的父類倦踢,如下所示侠草。此處用到了面向?qū)ο蟮亩鄳B(tài)性边涕,并且也用到了面向接口原則褂微。此處SuperTableViewCell雖然是一個基類园爷,但是它也擔(dān)負(fù)著定義子類接口的責(zé)任童社。好處就不多說了吧。
關(guān)于設(shè)計模式相關(guān)的內(nèi)容呀癣,請查看之前發(fā)布的關(guān)于設(shè)計模式的系列博客《設(shè)計模式系列》项栏,重構(gòu)的內(nèi)容的話請查看之前發(fā)布重構(gòu)系列的博客《重構(gòu)系列》蹬竖。當(dāng)然這兩個系列的博客全是使用Swift語言實現(xiàn)的Demo,不過思想都是相同的。好了今天博客篇幅也挺長的劈榨,就先到這兒吧晦嵌。
github分享鏈接:https://github.com/lizelu/DisplayTestDemo