如何加強 iOS 里的列表滾動時的順暢感?

例如 Path 的列表滾動感覺非常流暢,沒有 UITableView 的遲滯感(當然是指同樣多子 view 的情況下)。想知道 Path 用的是什麼方案?暫時只想到可能是完全純自繪。其他還有嗎?希望有相關經驗者不吝賜教


如果你想要如絲般順滑的效果,那麼:

1、每次都看一下有沒有能重用的 cell,而不是永遠重新新建(這個是 UITableView 的常識)

2、Cell 里盡量不要用 UIView 而是全部自己用 drawRect 畫(之前因為 iOS 有 bug,這樣做會有性能上質的飛越。也有很多大神寫過很多文章解釋原理,有興趣的自己去看看吧我就不做複製粘貼了。後來 iOS 也改掉了這個問題,這麼做的效果就沒那麼明顯了。)

3、圖片載入放到後台進程去進行,滾出可視範圍的載入進程要 cancel 掉

4、圓角、陰影之類的全部 bitmap 化,或者放到後台 draw 好了再拿來用

5、Cell 里要用的數據提前緩存好,不要現用現去讀文件

6、數據量太大來不及一次讀完的做一個 load more cell 出來,盡量避免邊滾邊讀數據,這樣就算是雙核的 CPU 也難保不會抽

做到以上6條的話,應該就能做出很順暢的滾動了(現在的 Twitter 官方客戶端的原作者寫過一篇文章,總結起來也無非就是我說的前3條,可以找來看看)。

Path 2.5 的那個滾動說實在的不是很順暢,圖片顯示出來的時候都會抽一下,他們還有很大的改進餘地。

對於3的補充說明:UIImageView 的載入是惰性的說法,是對的。但是大部分開發者都沒有正確理解這一點。下面就詳細解釋一下:

[UIImage imageWithContentOfFile:] 出來的 UIImage 其實並沒有真正把文件解碼到內存,而是要等到用的時候(例如去顯示或者去 scale)才會去做這件事情。但問題就在於 UIImageView 試圖去 draw 圖片的時候,它讀文件、渲染也是在主線程里做的,所以你要讀入的圖片如果很大(比如 iPad3 上的 @2x 圖),這一步就很容易會卡一下。這也就是為什麼我說圖片要放到後台進程去解碼完之後(解碼是可以後台做的!很多「大神」居然都不知道這一點),再拿來顯示(顯示只能在主線程做)的原因。


2012年的WWDC的238 section講的就是一些關於增強圖形動畫性能的tips,我做了一些筆記,我又整理了一下,樓主可以參考:

1、關於圖片的載入:

  • UIImageView 是由CALayer, UIImage-&>CGImage 構成的,CGImage 在載入的時候不會解碼圖像,只有在第一次用的時候才會解碼圖像(Lazy Loading)。所以,盡量用UIImageView 不要直接把圖像畫在 drawrect:
  • iOS本身對於PNG文件進行了很多優化:例如(這個我怕翻不太准):
  1. Premultiply alpha, and byte-swap

  2. Turn off some PNG compression modes

  3. Allow concurrent decoding of a single image
  • 此外,iOS6對Jpeg文件也已經優化了很多,但是不建議用Jpeg文件作為UI元素;永遠不要用其他格式的圖片作為UI元素,儘管iOS都支持。

  • [UIImage imageNamed:] 的數據會緩存在內存中,而[UIImage imageWithContentOfFile:]則不會,所以需要慎重選擇方法。這也是為什麼有時候模擬器跑得很好,而真機跑的時候很悲劇的原因,模擬器設置的緩存內存比真機大很多。
  • 在設置背景圖片的時候不要在drawRect里

    [self.image drawInRect: [self bounds] blendMode kCGBlendModeNormal alpha:1.0], 可以這樣:myView.layer.content = (id)[self.image CGImage];
  • iOS 6新功能 myView.layer.drawAsynchronously = YES 對於一個view里有很多需要draw的內容來說,很有用,但是有時候會很差,需要用time profile驗證以後再嘗試.

  • 在需要用到SetNeedDisplay的時候,看能不能用setNeedsDisplayInRect: 代替,這個會節省很多開銷!!!

2、關於Scrolling

  • 學會用Instruments!!!
  • 所有scrolling都需要 60 fps 小於 45fps用戶依然能察覺。所以我們擁有的時間僅有: 16ms/frame
  • 考慮優化功能的部分是在GPU還是CPU

    1. CGDrawing 和 imageIO 是CPU

    2. 渲染系統並不每一幀都工作在CPU上

    3. 渲染本身是在GPU上的

    4. 可以用OpenGL ES instruments 上的device utilization查看GPU使用情況

    如果是100%左右的,肯定是GPU

    如果是16%之類的,就應該是CPU

    如果是GPU,有一個 Core Animation instruments可以查看

    在scrolling 的 16ms 中,我們需要做的是

    1. calculate new scrolling position

    2. Prepare and commit animation

    3. Render frame

    減少blending,blending就是經常需要多重畫的

    self.layer.shouldRasterize = YES

    在Time profiler 里,如果有很多時間被浪費在了spring board 里,spring board實際是render server的所在,所以,結論應該是,應用里有太多的layer了。

    結論:

    1、多在不同設備上測試動畫、他們的區別可能在於GPU, CPU, Retina blabla

    2、不同場景有不同的解決方法,到底是用drawRect? 還是用SubView?

    3、測量、測試、迭代

    工作時間,先貼上來,稍後再整理=。=


tableView滾動不流暢涉及的原因是方方面面的, 其中複雜的高度計算也是令滾動卡頓的多發原因之一, 如果你的cell的高度相當複雜, 而且你又不得不在viewController的tableView代理方法中處理這些高度邏輯, 這將導致viewController代碼臃腫之餘, 也會成為tableView卡頓的重要原因

為此wwdc有專門的一期講述了如何合適的設置rowHeight的策略, 就是self-sized cell

如圖所示, row123是已經在屏幕上被展示的cell, 而row4則是下一個會被展示的cell, 這時row4這個cell的rowHeight是你預先為他設置的estimated height, 又或者是UITableViewDelegate中返回的height.

當用戶滾動的時候, 首先, cell就會被創建(或重用), 然後, cell會被調用調整size的方法, 接著, cell會根據tableView的size去調整自身的contentSize, 最後, cell被展示出來. 這就是self-sized cell的思路.

如下圖所示, 整個控制器中即使沒有任何UITableViewDelegate的方法, 也沒有額外的關於高度的私有方法, 只是簡單的賦值.也能達到根據文字長度自動調整高度的效果, 並且不會因為複雜的高度計算而導致卡頓.

使用這種策略, 所有的高度邏輯都被放在cell裡面, 你在tableView返回cell的方法中, 只需要簡單賦值給cell, 例如不同長度的文字, cell便會根據你所賦值的內容自動調整高度

以上的4步中, 最關鍵的是第二步. 如何讓一個cell去size自己的大小呢?

首先你的cell要使用Autolayout布局, 以上面的demo為例, 幫label簡單設置上下左右四個約束就可以了. 設置好了以後label能正常的顯示, 但是tableView仍然沒辦法根據內容的長度來調整cell的高度.

以上是wwdc的原裝代碼, 使用了VFL這種蛋疼玩意, 其實用xib簡單地拖四個到邊緣約束也是沒問題的

這是因為雖然cell已經具有自我布局的功能, 但是tableView還是以前那個tableView, 它仍然會去尋找自身的rowHeight屬性或者返回height的代理方法來確認每一行的高度.

所以最後一步是在tableView中啟動動態布局, 告訴tableView用新的方法來布局行高.

在tableView的初始化方法中加入以上代碼, estimatedRowHeight是為cell提供一個預估的行高, 還有一個作用是根據預算行高和行數, 顯示合理長度的scrollIndicator (灰色的滾動條) , 而UITableViewAutomaticDimension則是告訴tableView你將通過別的方法來算出行高, 而不是rowHeight或者delegate方法.再運行一下, 你將得到一個在沒有任何delagete方法設置行高的viewController中, 布局出根據內容有動態高度的tableView.


一句話終結此題,http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/


自己draw不會對滑動性能有明顯提升,因為UIView本身是輕量級的,而且UIView自身的繪製已經被優化到了極致,大部分時候自己draw的不會比UIView自身畫得快,跟繪圖的時間相比,UIView自身的初始化的性能損耗可以忽略不記。

rasterize是必備的,但對第一次繪製的cell沒有作用。而且rasterize再cell的外觀改變後,就要重畫,對於內容可變的cell,rasterize反而會降低性能。

補充幾點:

1.中文的繪製比英文要低效很多,如果可能,儘可能的讓中文少顯示一些,比如說在cell中加入一個「展開」按鈕,只有用戶點擊展開才能看到較大量的文字內容。

2.如果可以的化,要讓cell和tableview opaque,透明的view比較損耗性能。

3.儘可能的使用1:1像素的圖片,不要進行縮放,如果是與web端交互的應用,最好能專門為ios客戶端設置拉取小圖的介面。


簡單來說就是不要在主線程做 I/O 操作,這個其實很容易實現;對於有圖片的 Cell,最好在其他工作線程先把圖片的解碼工作做了,較大的圖片 downscale 一下。圓角陰影更是不要簡單粗暴地使用 CALayer 的那幾個屬性。如果能做到這些,繪製方面對界面造成的卡頓基本也不會有了。另外的優化點在於 Auto Layout 和 Cell Size 的計算上,對於布局複雜的 Cell,可以緩存 Cell 高度,或者優化布局。

UITableView 的優化基本就在於上面說的繪製和布局上。

另外是 iOS 10 中新增的特性,比如滾動時載入數據,可能會造成界面卡頓一下,利用 iOS 10 的 prefetch 特性可以使這個情況得以改善。


最簡單粗暴的方法,非同步載入


  1. UITableViewCell里不要添加太多subview,最好只添加一個cellview。
  2. UITableViewCell 上的子View的opaque屬性設為YES。其實默認也是不透明。UITableViewCell盡量不要包含透明的子View。
  3. 在cellview里,重寫drawRect函數繪製UITableViewCell的內容。
  4. 在繪製字元串時,儘可能使用drawAtPoint: withFont:,而不要使用更複雜的drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withFont:(UIFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode; 如果要繪製過長的字元串,建議自己先截斷,然後使用drawAtPoint: withFont:方法繪製。
  5. 在繪製圖片時,盡量使用drawAtPoint,而不要使用drawInRect。drawInRect如果在繪製過程中對圖片進行放縮,會特別消耗CPU。
  6. 如果繪製cell過程中,需要下載cell中的圖片,建議在繪製cell一段時間後再開啟圖片下載任務。譬如先畫一個默認圖片,然後在0.5S後開始下載本cell的圖片。
  7. 即使下載cell 圖片是在子線程中進行,在繪製cell過程中,也不能開啟過多的子線程。最好只有一個下載圖片的子線程在活動。否則也會影響UITableViewCell的繪製,因而影響了UITableViewCell的滑動速度。(建議結合使用NSOpeartion和NSOperationQueue來下載圖片,如果想儘可能找的下載圖片,可以把[self.queuesetMaxConcurrentOperationCount:4];)
  8. 最好自己寫一個cache,用來緩存UITableView中的UITableViewCell,這樣在整個UITableView的生命周期里,一個cell只需繪製一次,並且如果發生內存不足,也可以有效的釋放掉緩存的cell。
  9. 不要將tableview的背景顏色設置成一個圖片。這回嚴重影響UITableView的滑動速度。在限時免費搜索里,我曾經翻過一個錯誤:self.tableView_.backgroundColor = [UIColorcolorWithPatternImage:[UIImageimageNamed:@"background.png"]]; 通過這種方式設置UITableView的背景顏色會嚴重影響UTIableView的滑動流暢性。修改成self.tableView_.backgroundColor = [UIColor clearColor];之後,fps從43上升到60左右。滑動比較流暢。
  10. cell的行高不是固定值,需要計算,則要儘可能緩存行高值,避免重複計算行高。這裡指的是UITableViewDelegate里的行高函數。

如果做到以上10點,則UITableView 滑動的fps可以達到60 fps。滑動非常順暢...


隨手搬運。

iOS 保持界面流暢的技巧

這篇文章會非常詳細的分析 iOS 界面構建中的各種性能問題以及對應的解決思路,同時給出一個開源的微博列表實現,通過實際的代碼展示如何構建流暢的交互。


1、Path裡面有多種Cell,使用了多個CellIdentifier,並將其放在同一個TableView裡面來實現多樣式的載入;

2、Path並沒有避免透明背景的繪製,所以在透明背景的Label很多的情況下會比較卡,而當整個頁面都是圖片和評論(評論是白色背景的),流暢度會有明顯的提升;

3、Path的圖片比較大,多為延遲載入;

4、評論中小頭像的圓角使用圖片遮蓋來實現,避免了過多的繪製圓角。


設定CALayer的ShouldRasterize = YES成了簡易得到速度提升的小技巧。不過方便的事物總伴隨著局限性。雖然apple官方開發文件僅僅很保守的提及了透明度效果無法進行Rasterize(光柵化),然而在真機上的執行卻會造成圖像破碎跟閃爍的結果。偏偏透明度的應用在時髦介面的設計上很常見。 所有 : 慎用ShouldRasterize。 簡單的大招往往有副作用 且 未必適合所有場景, 尤其cell的內容不斷變化。


Path 開源了這部分的代碼,有興趣的可以研究研究

path/FastImageCache · GitHub


請問, Path裡面有多種Cell,使用了多個CellIdentifier,並將其放在同一個TableView裡面來實現多樣式的載入;

如果cell顯示內容相近,比方說一個cell裡面可能顯示了圖片並且多顯示一個label , 一個cell裡面沒有顯示圖片並少顯示一個label, 這種情況下,使用一個cell, 將裡面的view依據數據來源做.hidden屬性的設置, 還是做兩個不同的cell(兩個不同的CellIdentifier)效果好啊?


歸納:1. 非同步載入 2.圖形優化


不詳細展開了,請記住一個宗旨,所有所謂的為了流暢性的優化,都是將同步性行為改為非同步性行為。


推薦閱讀:

是不是 iOS 代碼手寫界面布局已經被判了死緩?
遊戲開發中的一個小問題(設計模式)?
求推薦初學者iOS開發APP學習參考書,目標兩個月內開發一個成熟的APP?
已經有了__weak 為什麼還要保留 __unsafe_unretained ?

TAG:iOS | iOS開發 | UITableView |