原文由Alexander Orlov發(fā)表于medium崩掘,地址為https://medium.com/ios-os-x-development/perfect-smooth-scrolling-in-uitableviews-fd609d5275a5#.so9tpnlk1
內(nèi)建方法
我相信大多數(shù)閱讀這篇文章的人都知道這些方法态秧,但一些人,即便是使用過這些方法咙轩,也沒有以正確的姿式來使用它們获讳。
******
首先是重用cell/header/footer的單個實例,即便是我們需要顯示多個活喊。這是優(yōu)化UIScrollView(UITableView的父類)最明顯的方式丐膝,UIScrollView是由蘋果的工程師提供的。為了正確的使用它钾菊,你應該只有cell/header/footer類帅矗,一次性初始化它們,并返回給UITableView结缚。
在蘋果的開發(fā)文檔里面已經(jīng)描述了重用cell的流程损晤,在這就沒有必須再重復了。
但重要的事情是:在UITableView的dataSource中實現(xiàn)的tableView:cellForRowAtIndexPath:方法红竭,需要為每個cell調(diào)用一次,它應該快速執(zhí)行。所以你需要盡可能快地返回重用cell實例茵宪。
不要在這里去執(zhí)行數(shù)據(jù)綁定最冰,因為目前在屏幕上還沒有cell。為了執(zhí)行數(shù)據(jù)綁定稀火,可以在UITableView的delegate方法tableView:willDisplayCell:forRowAtIndexPath:中進行暖哨。這個方法在顯示cell之前會被調(diào)用。
******
第二點也不難理解凰狞,但是有一件事需要解釋一下篇裁。
這個方法對于cell定高的UITableView來說沒有意義,但如果由于某些原因需要動態(tài)高度的cell的話赡若,這個方法可以很容易地讓滑動更流暢达布。
正如我們所知,UITableView是UIScrollView的子類逾冬,而UIScrollView的作用是讓用戶可以與比屏幕實際尺寸更大的區(qū)域交互黍聂。任何UIScrollView的實例都使用諸如contentSize、contentOffset和其它許多屬性來將正確的區(qū)域顯示給用戶身腻。
但是UITableView的問題在哪产还?正如所解釋的一樣,UITableView不會同時維護所有cell的實例嘀趟。相反脐区,它只需要維護顯示給用戶的那些cell。
那么她按,UITableView是如何知道它的contentSize呢坡椒?它是通過計算所以cell的高度之和來計算contentSize的值。
UITableView的delegate方法tableView:heightForRowAtIndexPath:會為每個cell調(diào)用一次尤溜,所以你應該非尘蟮穑快地返回高度值。
很多人會犯一個錯誤宫莱,他們會在布局初始化cell實例并綁定數(shù)據(jù)后去獲取它們的高度丈攒。如果你想優(yōu)化滑動的性能,就不應該以這種方式來計算cell的高度授霸,因為這事難以置信的低效巡验,iOS設(shè)備標準的60 FPS將會降低到15-20 FPS,滑動會變得很慢碘耳。
如果我們沒有一個cell的實例显设,那如何去計算它的高度呢?這里有一段示例代碼辛辨,它使用類方法捕捂,并基于傳入的寬度及顯示的數(shù)據(jù)來計算高度值:
可以用以下方式來使用上面這個方法返回高度值給UITableView:
你在實現(xiàn)這一切的時候能獲得了多少樂趣呢瑟枫?大多數(shù)人會說沒有。我沒有保證過這事很容易指攒。當然慷妙,我們可以構(gòu)建我們自己的類來手動布局和計算高度,但有時候我們沒有足夠的時間來做這件事允悦。你可以在Telegram的iOS應用代碼中找到這種實現(xiàn)的例子膝擂。
從iOS 8開始,我們可以在UITableView的delegate中使用自動高度計算隙弛,而不需要實現(xiàn)上面提到的方法架馋。為了實現(xiàn)這一功能,你可能會使用AutoLayout全闷,并將rowHeight變量設(shè)置為UITableViewAutomaticDimension叉寂。可以在StackOverflow中找到更多詳細的信息室埋。
盡管可以使用這些方法办绝,但我強烈建議不要使用它們。另外姚淆,我也不建議使用復雜的數(shù)學計算來獲取cell的高度孕蝉,如果可能,只使用加腌逢、減降淮、乘、除就可以搏讶。
但如果是AutoLayout呢佳鳖?它真的跟我所說的一樣慢么?你可能會很驚訝媒惕,但這是事實系吩。如果你想讓你的App在所有設(shè)備上都能平滑的滾動,你就會發(fā)現(xiàn)這種方法難以置信的慢妒蔚。你使用的子視圖越多穿挨,AutoLayout的效率越低。
AutoLayout相對低效的原因是隱藏在底層的命名為”Cassowary“的約束求解系統(tǒng)肴盏。如果布局中子視圖越多科盛,那么需要求解的約束也越多,進而返回cell給UITableView所花的時間也越多菜皂。
哪一個更快呢:使用少量的值來執(zhí)行基本的數(shù)學計算废麻,還是找一個求解大量線性等式或不等式的系統(tǒng)么戒祠?現(xiàn)在想像一下,用戶想要快速地滑動,每個cell的自動布局也執(zhí)行著瘋狂的計算蚁堤。
******
使用內(nèi)建方法優(yōu)化UITableView的正確方法是:
重用cell實例:對于特殊類型的cell,你應該只有一個實例,而沒有更多。
不要在cellForRowAtIndexPath:方法中綁定數(shù)據(jù)弹渔,因為在此時cell還沒有顯示胳施∷莼觯可以使用UITableView的delegate中的tableView:willDisplayCell:forRowAtIndexPath:方法。
快速計算cell高度舞肆。對于工程師來說這是常規(guī)工作焦辅,但你將會為優(yōu)化復雜cell的平滑滑動所付出的耐心而獲取回報。
我們需要更深一步
當然椿胯,上面提到的這些點不足以實現(xiàn)真正的平滑滾動筷登,特別是當你需要實現(xiàn)一些復雜的cell(如有大量的漸變、視圖哩盲、交互元素前方、一些修飾元素等等)時,這變得尤其明顯廉油。
這種情況下惠险,UITableView很容易變得緩慢,即便是做了上面所有的事情抒线。UITableViewCell中的視圖越多班巩,滑動時FPS越低。但在使用了手動布局和優(yōu)化了高度計算后嘶炭,問題就不在布局了抱慌,而在渲染了。
******
讓我們把關(guān)注點放在UIView的opaque屬性上眨猎。文檔中說它用于輔助繪圖系統(tǒng)定義UIView是否透明抑进。如果不透明,則繪圖系統(tǒng)在渲染視圖時可以做一些優(yōu)化睡陪,以提高性能寺渗。
我們需要性能,或者不是宝穗?用戶可能快速地滑動table户秤,如使用scrollsToTop特性,但他們可能沒有最新的iPhone逮矛,所以cell必須快速地被渲染鸡号。比通常的視圖更快。
渲染最慢的操作之一是混合(blending)须鼎【ò椋混合操作由GPU來執(zhí)行府蔗,因為這個硬件就是用來做混合操作的(當然不只是混合)。
你可能已經(jīng)猜到汞窗,提高性能的方法是減少混合操作的次數(shù)姓赤。但在此之前,我們需要找到它仲吏。讓我們來試試不铆。
在iOS模擬器上運行App,在模擬器的菜單中選擇’Debug‘裹唆,然后選中’Color Blended Layers‘誓斥。然后iOS模擬器就會將全部區(qū)域顯示為兩種顏色:綠色和紅色。
綠色區(qū)域沒有混合许帐,但紅色區(qū)域表示有混合操作劳坑。
正如你所看到的一樣,在cell中至少有兩處執(zhí)行了混合操作成畦,但你可能看不出差別來(這個混合操作是不必要的)距芬。
每種情況都應該仔細研究,不同的情況需要使用不同的方法來避免混合循帐。在我這里框仔,我需要做的只是設(shè)置backgroundColor來實現(xiàn)非透明。
但有時候可能更復雜惧浴〈婧停看看這個:我們有一個漸變,但是沒有混合衷旅。
如果想要使用CAGradientLayer來實現(xiàn)這個效果捐腿,你將會很失望:在iPhone 6中FPS將會降到25-30,快速滑動變得不可能柿顶。
這確實發(fā)生了茄袖,因為我們混合了兩個不同層的內(nèi)容:UILabel的CATextLayer和我們的CAGradientLayer。
如果能正確地利用了CPU和GPU資源嘁锯,它們將會均勻地負載宪祥,F(xiàn)PS保持在60幀〖页耍看起來就像下面這樣:
當設(shè)備需要執(zhí)行很多混合操作時蝗羊,問題就出現(xiàn)了:GPU是滿載的,但CPU卻保持低負載仁锯,而顯得沒有太大用處耀找。
大多數(shù)工程師在2010年夏季末時都面臨這個問題,當時發(fā)布了iPhone 4业崖。Apple發(fā)布了革命性的Retina顯示屏和…非常普通的GPU野芒。然而蓄愁,通常情況下它仍然有足夠的能力,但上面描述的問題卻變得越來越頻繁狞悲。
你可以在當前運行iOS 7系統(tǒng)的iPhone 4上看到這一現(xiàn)象—所有的應用都變得很慢撮抓,即使是最簡單的應用。不過摇锋,應用這篇文章中的介紹的方法丹拯,即使是在這種情況下,你的應用也能達到60 FPS乱投,盡管會有些困難咽笼。
所以顷编,需要怎么做呢戚炫?事實上,解決方案是:使用CPU來渲染媳纬!這將不會加載GPU双肤,這樣就無法執(zhí)行混合操作。例如钮惠,在執(zhí)行動畫的CALayer上茅糜。
我們可以在UIView的drawRect:方法中使用CoreGraphics操作來執(zhí)行CPU渲染,如下所示:
這段代碼nice么素挽?我會告訴你并非如此蔑赘。甚至通過這種方式,你會撤銷在一些UIView上(在任何情況下预明,它們都是不必要的)的所有緩存優(yōu)化操作缩赛。但是,這種方法禁用了一些混合操作撰糠,卸載GPU酥馍,從而使UITableView的更順暢。
但是記自睦摇:這提高了渲染性能旨袒,不是因為CPU比GPU更快!它可以讓我們通過為讓CPU來執(zhí)行某些渲染任務术辐,從而卸載GPU砚尽,因為在很多情況下,CPU可能不是100%負載的辉词。
優(yōu)化混合操作的關(guān)鍵點是在平衡CPU和GPU的負載必孤。
******
優(yōu)化UITableView中繪制數(shù)據(jù)操作的小結(jié):
減少iOS執(zhí)行無用混合的區(qū)域:不要使用透明背景,使用iOS模擬器或者Instruments來確認這一點较屿;如果可以隧魄,盡量使用沒有混合的漸變卓练。
優(yōu)化代碼,以平衡CPU和GPU的負載购啄。你需要清楚地知道哪部分渲染需要使用GPU襟企,哪部分可以使用CPU,以此保持平衡狮含。
為特殊的cell類型編寫特殊的代碼顽悼。
像素獲取
你知道像素看起來是什么樣的么?我的意思是几迄,屏幕上的物理像素是什么樣的蔚龙?我肯定你知道,但我還是想讓你看一下:
不同的屏幕有不同的制作工藝映胁,但有一件事是一樣的木羹。事實上,每個物理像素由三個顏色的子像素組成:紅解孙、綠坑填、藍。
基于這一事實弛姜,像素不是原子單位脐瑰,雖然對于應用來說它是⊥⒕剩或者仍然不是苍在?
直到帶有Retina屏的iPhone 4發(fā)布前,物理像素都可以用整型點坐標來描述荠商。自從有了Retina屏后寂恬,在Cocoa Touch環(huán)境下,我們就可以用屏幕點來取代像素了结啼,同時屏幕點可以是浮點值掠剑。
在完美的世界中(我們嘗試構(gòu)建的),屏幕點總是被處理成物理像素的整型坐標郊愧。但在現(xiàn)實生活中它可能是浮點值朴译,例如,線段可能起始于x為0.25的地方属铁。這時候眠寿,iOS將執(zhí)行子像素渲染。
這一技術(shù)在應用于特定類型的內(nèi)容(如文本)時很有意義焦蘑。但當我們繪制平滑直線時則沒有必要盯拱。
如果所有的平滑線段都使用子像素渲染技術(shù)來渲染,那你會讓iOS執(zhí)行一些不必要的任務,從而降低FPS狡逢。
******
什么情況下會出現(xiàn)這種不必要的子像素抗鋸齒操作呢宁舰?最常發(fā)生的情況是通過代碼計算而變成浮點值的視圖坐標,或者是一些不正確的圖片資源奢浑,這些圖片的大小不是對齊到屏幕的物理像素上的(例如蛮艰,你有一張在Retina顯示屏上的大小為60*61的圖片,而不是60*60的)雀彼。
在前面我們講到壤蚜,要解決問題,首先需要找到問題在哪徊哑。在iOS模擬器上運行程序袜刷,在”Debug“菜單中選中”Color Misaligned Image“。
這一次有兩種高亮區(qū)域:品紅色區(qū)域會執(zhí)行子像素渲染莺丑,而黃色區(qū)域是圖片大小沒有對齊的情況著蟹。
那如何在代碼中找到對應的位置呢?我總是使用手動布局窒盐,并且部分會自定義繪制草则,所以通常找到這些地方?jīng)]有任何問題。如果你使用Interface Builder蟹漓,那我對此深表同情。
通常源内,為了解決這個問題葡粒,你只要簡單地使用ceilf,?floorf和CGRectIntegral方法來對坐標做四舍五入處理。就是這樣膜钓!
******
通過上面的討論嗽交,我想建議你以下幾點:
對所有像素相關(guān)的數(shù)據(jù)做四舍五入處理,包括點坐標颂斜,UIView的高度和寬度夫壁。
跟蹤你的圖像資源:圖片必須是像素完美的,否則在Retina屏幕上渲染時沃疮,它會做不必要的抗鋸齒處理盒让。
定期復查你的代碼,因為這種情況可以會經(jīng)常出現(xiàn)司蔬。
異步UI
可能這看起來有點奇怪邑茄,但這是一種非常有效的方法。如果你知道如何做俊啼,那么可以讓UITableView滑動得更平滑肺缕。
現(xiàn)在我們來討論一下你應該做什么,然后再討論下你是否可能這么做。
******
每個中等以上規(guī)模的應用都可能會使用帶有媒體內(nèi)容的cell:文本同木、圖片浮梢、動畫,甚至還有視頻彤路。
而所有這些都可能帶有裝飾元素:圓角頭像黔寇、還’#‘號的文本、用戶名等斩萌。
我們已經(jīng)多次提及盡可能快地返回cell的需求缝裤,而在這里有一些麻煩:clipsToBounds很慢,圖片需要從網(wǎng)絡(luò)加載颊郎,需要在字符串中定位#號憋飞,和許多其它的問題。
優(yōu)化的目標是很明確的:如果在主線程中執(zhí)行這些操作姆吭,則會讓你不能很快地返回cell榛做。
在后臺加載圖片,在相同的地方處理圓角内狸,然后將處理后的圖片指定給UIImageView检眯。
立刻顯示文本,但在后臺定位#號昆淡,然后使用屬性字符串來刷新顯示锰瘸。
在你的cell中,需要具體情況具體分析昂灵,但主要的思想是在后臺執(zhí)行大的操作避凝。這可能不止是網(wǎng)絡(luò)代碼,你需要使用Instruments來找到它們眨补。
記坠芟鳌:需要盡快返回cell。
******
有時候撑螺,上面的所有技術(shù)可能都幫不上忙含思。如GPU仍然不能使用(iPhone4+iOS7)時,cell中有很多內(nèi)容時甘晤,需要CALayer的支持以實現(xiàn)動畫時(因為在drawRect:中實現(xiàn)起來真的很麻煩)含潘。
在這種情況下,我們需要在后臺渲染所有其它東西安皱。此外它能在用戶快速滑動UITableView時有效地提高FPS调鬓。
我們來看看Facebook的應用。為了檢測這些酌伊,你可能需要往下滑足夠的高度腾窝,然后點擊狀態(tài)欄缀踪。列表會往上滑動,因此你可以清楚地看到此時沒有渲染cell虹脯。如果想要更精確驴娃,則不能及時獲得。
這很簡單循集,所以你可以自己試試唇敞。這時,你需要設(shè)置CALayer的drawsAsynchronously屬性為YES咒彤。
但是我們可以檢查這些行為的必要性疆柔。在iOS模擬器上運行程序,然后選擇“Debug”菜單中的”Color Offscreen-Rendered“∠庵現(xiàn)在所有在后臺渲染的區(qū)域都被高亮為黃色旷档。
如果你為某些層開啟了這一模式,但是它沒有高亮顯示歇拆,那么它就不夠慢鞋屈。
為了在CALyaer層找到瓶頸并進一步減少它,你可以使用Instruments里面的Time Profiler故觅。
******
這里是異步化UI的實現(xiàn)清單:
找到讓你的cell無法快速返回的瓶頸厂庇。
將操作移到后臺線程,并在主線程刷新顯示的內(nèi)容输吏。
最后一招是設(shè)置你的CALayer為異步顯示模式(即使只是簡單的文本或圖片)—這將幫你提高FPS权旷。
結(jié)論
我嘗試解釋了iOS繪圖系統(tǒng)(沒有使用OpenGL,因為它的情況更少)的主要思路评也。當然有些看起來很模糊炼杖,但事實上這只是一些方向,你應該朝著這些方向來檢查你的代碼以找出影響滾動性能的所有問題盗迟。
具體情況具體分析,但原則是不變的熙含。
獲取完美平滑滾動的關(guān)鍵是非常特殊的代碼罚缕,它能讓你竭盡iOS的能力來讓你的應用更加平滑。