近期把滑動優化的一些經驗整理了一下,在公司做了一次技術分享,和我之前的文章有一小部分重疊。現摘要如下,希望大家不吝賜教,共同討論進步。
一.滑動優化的玄學
為什么說是玄學呢,因為大部分情況下的APP,用不到這些優化的點,過早的優化是惡魔,當真正出現性能問題的時候,再考慮這些方面的優化。
1.多個透明元素重疊顯示的性能問題。
- 解決方案:合并成一張圖顯示
- 原理:CPU方面,減少了UIKit的創建消耗,GPU方面,避免了合成渲染產生的消耗。
- AsyncDisplayKit(現在叫Texture),針對多個透明元素的重疊,預合并無點擊響應,不改變動畫的圖層。
- Texture的保持流暢的原理:UIKit不是線程安全的,所以必須在主線程改動。Texture利用中間變量存儲改動,保證線程安全,在合適的機會將并發操作同步到主線程。
- 暫時不用Texture的原因:需要用Texture Node Container替換UIKit元素,成本較大。
2.靜態cell、多圖待加載的優化
- 解決方案:合并成一張圖顯示;
- 原理:提升I/O速度,一個大文件的讀取速度,通常比多個小文件要快。
3.展示適合界面尺寸圖片,不進行拉伸縮放。
- 解決方案:從服務器拉取合適尺寸的圖片(例如七牛的服務就帶裁剪/壓縮參數);
- 原理:過大圖片對內存消耗巨大(圖片占用內存 = 圖像高×圖像寬×像素位數);不符合UIImageView尺寸的圖片,進行重新縮減/放大尺寸的消耗是非常巨大的。
4.imageNamed和imageWithContentsOfFile
這個知道的人比較多,因為緩存圖片的消耗通常是肉眼可見的多。
常用的元素例如icon之類的,采用imageNamed:,系統會有緩存。
如果是較大或者不常用的圖片資源,采用imageWithContentsOfFile:。
5.減少autolayout的使用
- 解決方案:頁面元素多的時候,減少autolayout布局,采用frame。
- 原理:元素多時,autolayout的消耗非常驚人(http://pilky.me/36/) ,之前看過搜狗的iOS分享,搜狗輸入法鍵盤彈出狂卡即是此原因;
6.獲取文件大小
- 解決方案:不要使用NSFileManager,用C的stat來獲取文件信息。
- 實例:獲取一個目錄下所有文件大小,進行多次遞歸計算,stat幾乎瞬間完成,NSFileManager耗時較長。
7.NSDateFormatter產生較大消耗
- 解決方案:.緩存NSDateFormatter結果,不多次創建,及時釋放。
- 做過類似日歷的同學應該都懂??
8.圖片解碼:
- 解決方案:CALayer 被提交到 GPU 前,CGImage 中的數據才會得到解碼,GPU執行,卡主線程。常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創建圖片。
- SDWebImage/YYImage等圖片庫都是這么做的,有興趣的同學可以去看下源碼。如果你是自己做圖片下載,就要考慮到相關優化。
二.cell高度預計算/緩存
- 一般情況下,不要用estimatedRowHeight,不然容易鬼畜;
- systemLayoutSizeFittingSize:這個方法,就是大部分cell布局庫采用的方法,只要從上至下布局全部生效,就能計算高度,不要多次調用;
- 由于UITableView繪制過程中多次調用繪制,所以緩存高度計算結果,可以有效的增加滑動流暢度;
- 當cell高度改變,記得及時替換緩存;
三.離屏渲染
觸發條件:CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示。
主要問題:GPU占滿,CPU空閑
解決方法:
1.開啟CALayer.shouldRasterize ,轉嫁到CPU上;
2.粗暴畫圖/截圖實現border和圓角;
3.砍死設計師。
最近拖延癥有點厲害,這篇文章想寫了很久都沒寫出來,我先摘要一部分思路出來。
曾經的需求是這樣的:
初期方案是圖片下載完成后,裁成圓形,然后外面用貝塞爾畫一個圈,根據不同的UI緩存不同多個帶圈兒的圖;
然而...神奇的產品第二版給我整出了無數個各種不同大小、間距、透明度的圈。這樣就意味著我得緩存無數帶著各色圈兒的圖。
后來突然靈光一閃,單獨設layer的圓角貌似不引發離屏渲染(有說法是iOS8之后)。所以后來方案變成,UIView嵌套一個UIImageView,只緩存裁剪過的圖片。
但是這樣的話,其實UIKit的創建要比讀取畫好的圖更耗性能。所以這是個終極的問題,時間/空間,究竟選哪個。
四.圖片的處理
1.SDWebImage的外層干了啥
這篇文章寫了1/4的樣子,詳細的會在這篇文章里,十分心疼自己。下面簡單說說SDWebImage的外層都干了啥:
- 重用cell的時候,cancel之前下載operation;
- 二級緩存:memory/disk;
- 合并回調,多次調用只回調一次;
2.項目中實際遇到的問題
我們做了個類似SDWebImage的東西,來實現神奇的需求,過程中遇到了一些問題:
-
從disk讀取需要時間,在memory沒有的情況下,導致image = nil的一瞬間閃動:我把SDWeb的demo改了改,左邊按鈕ReloadData,右邊按鈕清除內存(memory)緩存,在reload的一瞬間,展示了placeHolder的圖。
SDWebFlash.gif UICollectionView, reloadData 迷之移位閃爍問題:當reloadData的時候,重用的cell神奇的變換了次序,此時memory里只要一圖片不存在,就會有一瞬間的閃動。
-
上面兩個問題的解決方法有:
- 盡量避免reloadData,可以找到visibleCells,一一更換換圖片;
- 優化細節,在發現disk里面有圖的時候,image不設為nil,避免閃動;
- 保證memory緩存的大小和數量,能滿足界面需求;
請求沒有合并/回調沒有合并;
這個比較好解決,就是請求之前判斷該請求是否在執行,若在執行,則將回調暫存到該請求下,等完成后一并調用。
五.ScrollView滑動優化
1.滑動時的代理
- scrollViewDidScroll:實際上就是contentOffset的KVO
- scrollViewWillBeginDragging:
- scrollViewWillEndDragging: withVelocity: (points/millisecond這實際上是個速度的參數)targetContentOffset:(這是一個可以傳值的指針,可以控制最后的減速動畫)
- scrollViewDidEndDragging: willDecelerate:(拖動結束,如果仍有速度,會執行后面兩個方法)
- scrollViewWillBeginDecelerating:
- scrollViewDidEndDecelerating:
- 需要注意scrollView的dragging屬性在decelerate的過程中仍然為YES
2.特殊情況
drag完,正decelerating時(didEndDecelerating尚未調用),強行再次drag(單指停止滑動,雙指連環滑)
- 單指停止滑動:沒有decelerate ,willBeginDecelerating不會被調用,但前一次留下的 didEndDecelerating 會被調用(后面會結合VVWeibo的例子講述這里怎么處理)
- 雙指連環滑動:willBeginDecelerating會先于didEndDecelerating調用,就是說這種情況didEndDecelerating會在你手指離開屏幕且屏幕停止的情況下調用。
3.VVWeibo的做法
- scrollViewWillEndDragging: withVelocity: targetContentOffset:時,可以從targetContentOffset判斷即將加載的那一頁cell,從而預先加載,UITableView有傳入rect返回cells的方法,UICollectionView得強行取兩個點獲得這兩個點cell的IndexPath,然后得到cells。
- 遇到前文單指停止的處理,VVWeibo是在UITableView的子類捕獲了touchEvent,然后reloadData,我就沒有做子類了。最后做了一系列神奇的判斷,然后reload。但是仍然遇到了 reloadData 迷之移位閃爍問題;后來我加入了速度的判斷,這個已經不會觸發了,我就暫時注銷掉了,等待下一步優化。
4.最迷的問題
UICollectionViewLayout的prepareLayout調用了過多次數,是因為shouldInvalidateLayoutForBoundsChange:這個方法災難的調用了多次,newBounds的x和y實際上隨著滑動一直在變,return YES的話就一直重新布局,最后用magicNumber存他的size,當size變化才返回YES,就很強行的解決了。
六.魔鬼般的視頻播放
這里涉及業務邏輯過多,我也不方便多寫,就寫一些過程中遇到的問題:
scrollViewDidEndDecelerating的VisibleItems為nil。換個線程/延時去取,就能取到。
縮小播放區域,跟前面的取點找目標cell的操作類似,找出首尾點,中間的cell,即是需要播放的cell。
多個等待播放的AVPlayerItemVideoOutput,會導致一部分失效,內存越小的機子上越明顯。
解決方法:播放前再把url賦給playerItem,一定程度避免過多playerItem失敗的問題;出入頁面找到所有playerItem并干掉,避免影響其他播放;
GLKView的reuse在狂滑的時候十分耗內存,而不reuse的話,重用的時候,會顯示上一個頁面的殘影,解決方法是先用圖片蓋住殘影,在播前,清理上一次播放的殘余;
加入一個Timer,通過記錄偏移量來控制滑動速度,高速度的時候,不繪制/下載圖片。這樣也解決了雙指狂滑的時候,無法很好的判斷當前是否繪制的問題。
Conference
http://wereadteam.github.io/2016/05/03/WeRead-Performance/ 微信讀書 iOS 性能優化總結
http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/ iOS 保持界面流暢的技巧
http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/ 優化UITableViewCell高度計算的那些事
http://tech.glowing.com/cn/practice-in-uiscrollview/ UIScrollView 實踐經驗
《High Performance iOS Apps》這本真是神書,有興趣深入學習優化的可以去看看,中文的貌似有美團技術團隊翻譯的
簡書已經棄用,歡迎移步我的小專欄:
https://xiaozhuanlan.com/dahuihuiiOS