我已經在iOS這個最好的移動平臺上有幾年的開發經驗了。在這期間,我已以接觸過很多的iOS應用和iOS工程師。
我們的世界很多好的開發者,但有時我發現他們中的一些人並不是很清楚如何充分利用這個最受歡迎的移動裝置的整體潛力,來開發真正平滑的應用。
現在,我將嘗試從我的視角,來說明一下為了讓UITableView
更快更平滑,工程師應該做哪些優化。
文章越往後,難度和深度也會不斷增加,所以我將以一些你熟悉的東西來開始。文章後面將會討論iOS繪畫系統和UIKit更深層次的一些東西。
內建方法
我相信大多數閱讀這篇文章的人都知道這些方法,但一些人,即便是使用過這些方法,也沒有以正確的姿式來使用它們。
✻ ✻ ✻ ✻ ✻
首先是重用cell/header/footer
的單個例項,即便是我們需要顯示多個。這是優化UIScrollView
(UITableView
的父類)最明顯的方式,UIScrollView
是由蘋果的工程師提供的。為了正確的使用它,你應該只有cell/header/footer
類,一次性初始化它們,並返回給UITableView
。
在蘋果的開發文件裡面已經描述了重用cell
的流程,在這就沒有必須再重複了。
但重要的事情是:在UITableView
的dataSource
中實現的tableView:cellForRowAtIndexPath:
方法,需要為每個cell
呼叫一次,它應該快速執行。所以你需要儘可能快地返回重用cell
例項。
不要在這裡去執行資料繫結,因為目前在螢幕上還沒有
cell
。為了執行資料繫結,可以在UITableView
的delegate
方法tableView:willDisplayCell:forRowAtIndexPath:
中進行。這個方法在顯示cell
之前會被呼叫。
✻ ✻ ✻ ✻ ✻
第二點也不難理解,但是有一件事需要解釋一下。
這個方法對於cell
定高的UITableView
來說沒有意義,但如果由於某些原因需要動態高度的cell
的話,這個方法可以很容易地讓滑動更流暢。
正如我們所知,UITableView
是UIScrollView
的子類,而UIScrollView
的作用是讓使用者可以與比螢幕實際尺寸更大的區域互動。任何UIScrollView
的例項都使用諸如contentSize
、contentOffset
和其它許多屬性來將正確的區域顯示給使用者。
但是UITableView
的問題在哪?正如所解釋的一樣,UITableView
不會同時維護所有cell
的例項。相反,它只需要維護顯示給使用者的那些cell
。
那麼,UITableView
是如何知道它的contentSize
呢?它是通過計算所以cell
的高度之和來計算contentSize
的值。
UITableView
的delegate
方法tableView:heightForRowAtIndexPath:
會為每個cell
呼叫一次,所以你應該非常快地返回高度值。
很多人會犯一個錯誤,他們會在佈局初始化cell
例項並繫結資料後去獲取它們的高度。如果你想優化滑動的效能,就不應該以這種方式來計算cell
的高度,因為這事難以置信的低效,iOS裝置標準的60 FPS
將會降低到15-20 FPS
,滑動會變得很慢。
如果我們沒有一個cell
的例項,那如何去計算它的高度呢?這裡有一段示例程式碼,它使用類方法,並基於傳入的寬度及顯示的資料來計算高度值:
可以用以下方式來使用上面這個方法返回高度值給UITableView
:
你在實現這一切的時候能獲得了多少樂趣呢?大多數人會說沒有。我沒有保證過這事很容易。當然,我們可以構建我們自己的類來手動佈局和計算高度,但有時候我們沒有足夠的時間來做這件事。你可以在Telegram的iOS應用程式碼中找到這種實現的例子。
從iOS 8開始,我們可以在UITableView
的delegate
中使用自動高度計算,而不需要實現上面提到的方法。為了實現這一功能,你可能會使用AutoLayout
,並將rowHeight
變數設定為UITableViewAutomaticDimension
。可以在StackOverflow中找到更多詳細的資訊。
儘管可以使用這些方法,但我強烈建議不要使用它們。另外,我也不建議使用複雜的數學計算來獲取cell
的高度,如果可能,只使用加、減、乘、除就可以。
但如果是AutoLayout
呢?它真的跟我所說的一樣慢麼?你可能會很驚訝,但這是事實。如果你想讓你的App在所有裝置上都能平滑的滾動,你就會發現這種方法難以置信的慢。你使用的子檢視越多,AutoLayout
的效率越低。
AutoLayout
相對低效的原因是隱藏在底層的命名為”Cassowary
“的約束求解系統。如果佈局中子檢視越多,那麼需要求解的約束也越多,進而返回cell
給UITableView
所花的時間也越多。
哪一個更快呢:使用少量的值來執行基本的數學計算,還是找一個求解大量線性等式或不等式的系統麼?現在想像一下,使用者想要快速地滑動,每個cell
的自動佈局也執行著瘋狂的計算。
✻ ✻ ✻ ✻ ✻
使用內建方法優化UITableView
的正確方法是:
- 重用
cell
例項:對於特殊型別的cell
,你應該只有一個例項,而沒有更多。 - 不要在
cellForRowAtIndexPath:
方法中繫結資料,因為在此時cell
還沒有顯示。可以使用UITableView
的delegate
中的tableView:willDisplayCell:forRowAtIndexPath:
方法。 - 快速計算
cell
高度。對於工程師來說這是常規工作,但你將會為優化複雜cell
的平滑滑動所付出的耐心而獲取回報。
我們需要更深一步
當然,上面提到的這些點不足以實現真正的平滑滾動,特別是當你需要實現一些複雜的cell
(如有大量的漸變、檢視、互動元素、一些修飾元素等等)時,這變得尤其明顯。
這種情況下,UITableView
很容易變得緩慢,即便是做了上面所有的事情。UITableViewCell
中的檢視越多,滑動時FPS越低。但在使用了手動佈局和優化了高度計算後,問題就不在佈局了,而在渲染了。
✻ ✻ ✻ ✻ ✻
讓我們把關注點放在UIView
的opaque
屬性上。文件中說它用於輔助繪圖系統定義UIView
是否透明。如果不透明,則繪圖系統在渲染檢視時可以做一些優化,以提高效能。
我們需要效能,或者不是?使用者可能快速地滑動table,如使用scrollsToTop
特性,但他們可能沒有最新的iPhone
,所以cell
必須快速地被渲染。比通常的檢視更快。
渲染最慢的操作之一是混合(blending
)。混合操作由GPU來執行,因為這個硬體就是用來做混合操作的(當然不只是混合)。
你可能已經猜到,提高效能的方法是減少混合操作的次數。但在此之前,我們需要找到它。讓我們來試試。
在iOS模擬器上執行App,在模擬器的選單中選擇’Debug
‘,然後選中’Color Blended Layers
‘。然後iOS模擬器就會將全部區域顯示為兩種顏色:綠色和紅色。
綠色區域沒有混合,但紅色區域表示有混合操作。
正如你所看到的一樣,在cell
中至少有兩處執行了混合操作,但你可能看不出差別來(這個混合操作是不必要的)。
每種情況都應該仔細研究,不同的情況需要使用不同的方法來避免混合。在我這裡,我需要做的只是設定backgroundColor
來實現非透明。
但有時候可能更復雜。看看這個:我們有一個漸變,但是沒有混合。
如果想要使用CAGradientLayer
來實現這個效果,你將會很失望:在iPhone 6中FPS將會降到25-30
,快速滑動變得不可能。
這確實發生了,因為我們混合了兩個不同層的內容:UILabel
的CATextLayer
和我們的CAGradientLayer
。
如果能正確地利用了CPU
和GPU
資源,它們將會均勻地負載,FPS保持在60
幀。看起來就像下面這樣:
當裝置需要執行很多混合操作時,問題就出現了:GPU
是滿載的,但CPU
卻保持低負載,而顯得沒有太大用處。
大多數工程師在2010年夏季末時都面臨這個問題,當時釋出了iPhone 4。Apple釋出了革命性的Retina
螢幕和…非常普通的GPU
。然而,通常情況下它仍然有足夠的能力,但上面描述的問題卻變得越來越頻繁。
你可以在當前執行iOS 7系統的iPhone 4上看到這一現象—所有的應用都變得很慢,即使是最簡單的應用。不過,應用這篇文章中的介紹的方法,即使是在這種情況下,你的應用也能達到60 FPS
,儘管會有些困難。
所以,需要怎麼做呢?事實上,解決方案是:使用CPU
來渲染!這將不會載入GPU,這樣就無法執行混合操作。例如,在執行動畫的CALayer
上。
我們可以在UIView
的drawRect:
方法中使用CoreGraphics
操作來執行CPU
渲染,如下所示:
這段程式碼nice麼?我會告訴你並非如此。甚至通過這種方式,你會撤銷在一些UIView
上(在任何情況下,它們都是不必要的)的所有快取優化操作。但是,這種方法禁用了一些混合操作,解除安裝GPU
,從而使UITableView
的更順暢。
但是記住:這提高了渲染效能,不是因為CPU
比GPU
更快!它可以讓我們通過為讓CPU
來執行某些渲染任務,從而解除安裝GPU
,因為在很多情況下,CPU
可能不是100%負載的。
優化混合操作的關鍵點是在平衡CPU
和GPU
的負載。
✻ ✻ ✻ ✻ ✻
優化UITableView
中繪製資料操作的小結:
- 減少iOS執行無用混合的區域:不要使用透明背景,使用iOS模擬器或者
Instruments
來確認這一點;如果可以,儘量使用沒有混合的漸變。 - 優化程式碼,以平衡
CPU
和GPU
的負載。你需要清楚地知道哪部分渲染需要使用GPU
,哪部分可以使用CPU
,以此保持平衡。 - 為特殊的
cell
型別編寫特殊的程式碼。
畫素獲取
你知道畫素看起來是什麼樣的麼?我的意思是,螢幕上的物理畫素是什麼樣的?我肯定你知道,但我還是想讓你看一下:
不同的螢幕有不同的製作工藝,但有一件事是一樣的。事實上,每個物理畫素由三個顏色的子畫素組成:紅、綠、藍。
基於這一事實,畫素不是原子單位,雖然對於應用來說它是。或者仍然不是?
直到帶有Retina
屏的iPhone 4釋出前,物理畫素都可以用整型點座標來描述。自從有了Retina
屏後,在Cocoa Touch
環境下,我們就可以用螢幕點來取代畫素了,同時螢幕點可以是浮點值。
在完美的世界中(我們嘗試構建的),螢幕點總是被處理成物理畫素的整型座標。但在現實生活中它可能是浮點值,例如,線段可能起始於x
為0.25
的地方。這時候,iOS將執行子畫素渲染。
這一技術在應用於特定型別的內容(如文字)時很有意義。但當我們繪製平滑直線時則沒有必要。
如果所有的平滑線段都使用子畫素渲染技術來渲染,那你會讓iOS執行一些不必要的任務,從而降低FPS。
✻ ✻ ✻ ✻ ✻
什麼情況下會出現這種不必要的子畫素抗鋸齒操作呢?最常發生的情況是通過程式碼計算而變成浮點值的檢視座標,或者是一些不正確的圖片資源,這些圖片的大小不是對齊到螢幕的物理畫素上的(例如,你有一張在Retina
螢幕上的大小為60*61
的圖片,而不是60*60
的)。
在前面我們講到,要解決問題,首先需要找到問題在哪。在iOS模擬器上執行程式,在”Debug
“選單中選中”Color Misaligned Image
“。
這一次有兩種高亮區域:品紅色區域會執行子畫素渲染,而黃色區域是圖片大小沒有對齊的情況。
那如何在程式碼中找到對應的位置呢?我總是使用手動佈局,並且部分會自定義繪製,所以通常找到這些地方沒有任何問題。如果你使用Interface Builder
,那我對此深表同情。
通常,為了解決這個問題,你只要簡單地使用ceilf
, floorf
和CGRectIntegral
方法來對座標做四捨五入處理。就是這樣!
✻ ✻ ✻ ✻ ✻
通過上面的討論,我想建議你以下幾點:
- 對所有畫素相關的資料做四捨五入處理,包括點座標,
UIView
的高度和寬度。 - 跟蹤你的影像資源:圖片必須是畫素完美的,否則在
Retina
螢幕上渲染時,它會做不必要的抗鋸齒處理。 - 定期複查你的程式碼,因為這種情況可以會經常出現。
非同步UI
可能這看起來有點奇怪,但這是一種非常有效的方法。如果你知道如何做,那麼可以讓UITableView
滑動得更平滑。
現在我們來討論一下你應該做什麼,然後再討論下你是否可能這麼做。
✻ ✻ ✻ ✻ ✻
每個中等以上規模的應用都可能會使用帶有媒體內容的cell
:文字、圖片、動畫,甚至還有視訊。
而所有這些都可能帶有裝飾元素:圓角頭像、還’#‘號的文字、使用者名稱等。
我們已經多次提及儘可能快地返回cell
的需求,而在這裡有一些麻煩:clipsToBounds
很慢,圖片需要從網路載入,需要在字串中定位#號,和許多其它的問題。
優化的目標是很明確的:如果在主執行緒中執行這些操作,則會讓你不能很快地返回cell
。
在後臺載入圖片,在相同的地方處理圓角,然後將處理後的圖片指定給UIImageView
。
立刻顯示文字,但在後臺定位#
號,然後使用屬性字串來重新整理顯示。
在你的cell
中,需要具體情況具體分析,但主要的思想是在後臺執行大的操作。這可能不止是網路程式碼,你需要使用Instruments
來找到它們。
記住:需要儘快返回cell
。
✻ ✻ ✻ ✻ ✻
有時候,上面的所有技術可能都幫不上忙。如GPU
仍然不能使用(iPhone4+iOS7)時,cell
中有很多內容時,需要CALayer
的支援以實現動畫時(因為在drawRect:
中實現起來真的很麻煩)。
在這種情況下,我們需要在後臺渲染所有其它東西。此外它能在使用者快速滑動UITableView
時有效地提高FPS
。
我們來看看Facebook
的應用。為了檢測這些,你可能需要往下滑足夠的高度,然後點選狀態列。列表會往上滑動,因此你可以清楚地看到此時沒有渲染cell
。如果想要更精確,則不能及時獲得。
這很簡單,所以你可以自己試試。這時,你需要設定CALayer
的drawsAsynchronously
屬性為YES。
但是我們可以檢查這些行為的必要性。在iOS模擬器上執行程式,然後選擇“Debug
”選單中的”Color Offscreen-Rendered
“。現在所有在後臺渲染的區域都被高亮為黃色。
如果你為某些層開啟了這一模式,但是它沒有高亮顯示,那麼它就不夠慢。
為了在CALyaer
層找到瓶頸並進一步減少它,你可以使用Instruments
裡面的Time Profiler
。
✻ ✻ ✻ ✻ ✻
這裡是非同步化UI的實現清單:
- 找到讓你的
cell
無法快速返回的瓶頸。 - 將操作移到後臺執行緒,並在主執行緒重新整理顯示的內容。
- 最後一招是設定你的
CALayer
為非同步顯示模式(即使只是簡單的文字或圖片)—這將幫你提高FPS。
結論
我嘗試解釋了iOS繪圖系統(沒有使用OpenGL
,因為它的情況更少)的主要思路。當然有些看起來很模糊,但事實上這只是一些方向,你應該朝著這些方向來檢查你的程式碼以找出影響滾動效能的所有問題。
具體情況具體分析,但原則是不變的。
獲取完美平滑滾動的關鍵是非常特殊的程式碼,它能讓你竭盡iOS的能力來讓你的應用更加平滑。