如何加強 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文件進行了很多優化:例如(這個我怕翻不太准):
- Premultiply alpha, and byte-swap
- Turn off some PNG compression modes
- 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還是CPU1. CGDrawing 和 imageIO 是CPU2. 渲染系統並不每一幀都工作在CPU上3. 渲染本身是在GPU上的4. 可以用OpenGL ES instruments 上的device utilization查看GPU使用情況 如果是100%左右的,肯定是GPU 如果是16%之類的,就應該是CPU
如果是GPU,有一個 Core Animation instruments可以查看
在scrolling 的 16ms 中,我們需要做的是1. calculate new scrolling position2. Prepare and commit animation3. Render frame減少blending,blending就是經常需要多重畫的self.layer.shouldRasterize = YES在Time profiler 里,如果有很多時間被浪費在了spring board 里,spring board實際是render server的所在,所以,結論應該是,應用里有太多的layer了。結論:1、多在不同設備上測試動畫、他們的區別可能在於GPU, CPU, Retina blabla2、不同場景有不同的解決方法,到底是用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的方法, 也沒有額外的關於高度的私有方法, 只是簡單的賦值.也能達到根據文字長度自動調整高度的效果, 並且不會因為複雜的高度計算而導致卡頓.
以上的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 特性可以使這個情況得以改善。最簡單粗暴的方法,非同步載入
- UITableViewCell里不要添加太多subview,最好只添加一個cellview。
- UITableViewCell 上的子View的opaque屬性設為YES。其實默認也是不透明。UITableViewCell盡量不要包含透明的子View。
- 在cellview里,重寫drawRect函數繪製UITableViewCell的內容。
- 在繪製字元串時,儘可能使用drawAtPoint: withFont:,而不要使用更複雜的drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withFont:(UIFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode; 如果要繪製過長的字元串,建議自己先截斷,然後使用drawAtPoint: withFont:方法繪製。
- 在繪製圖片時,盡量使用drawAtPoint,而不要使用drawInRect。drawInRect如果在繪製過程中對圖片進行放縮,會特別消耗CPU。
- 如果繪製cell過程中,需要下載cell中的圖片,建議在繪製cell一段時間後再開啟圖片下載任務。譬如先畫一個默認圖片,然後在0.5S後開始下載本cell的圖片。
- 即使下載cell 圖片是在子線程中進行,在繪製cell過程中,也不能開啟過多的子線程。最好只有一個下載圖片的子線程在活動。否則也會影響UITableViewCell的繪製,因而影響了UITableViewCell的滑動速度。(建議結合使用NSOpeartion和NSOperationQueue來下載圖片,如果想儘可能找的下載圖片,可以把[self.queuesetMaxConcurrentOperationCount:4];)
- 最好自己寫一個cache,用來緩存UITableView中的UITableViewCell,這樣在整個UITableView的生命周期里,一個cell只需繪製一次,並且如果發生內存不足,也可以有效的釋放掉緩存的cell。
- 不要將tableview的背景顏色設置成一個圖片。這回嚴重影響UITableView的滑動速度。在限時免費搜索里,我曾經翻過一個錯誤:self.tableView_.backgroundColor = [UIColorcolorWithPatternImage:[UIImageimageNamed:@"background.png"]]; 通過這種方式設置UITableView的背景顏色會嚴重影響UTIableView的滑動流暢性。修改成self.tableView_.backgroundColor = [UIColor clearColor];之後,fps從43上升到60左右。滑動比較流暢。
- 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 |