起因
我們公司的主App在大約17年5月份前後經歷了一次大版本迭代,迭代之後更換了若干個一級和二級頁面,首頁就在這些個一級頁面之內。 17年大約11月份的時候,我們的小程式第一個版本正式上線,然後我們技術的大Leader拿來了小程式給我們看看,小程式的首頁流暢性確實優於我們客戶端,於是我們正式啟動了效能優化。
明確優化的目標
優化的第一步,肯定是要明確我們優化具體的Case,需要達到什麼樣的流暢度?是fps達到60?還是要記憶體使用降到一個具體的數字? 討論之後,我們最終將第一期優化定位為將首頁的fps優化到60。
調研過程
雖然說是第一期將目標定位優先優化首頁的流暢度,但是本著有第一次就要有第二次的原則,我還是將App中的其他流暢度敏感頁面也Review了一遍程式碼,算是給自己留一個優化思考的方向。
Review程式碼發現一些比較顯而易見的問題:
- 肆意呼叫資料庫而沒有用cache
- 複雜UI大量使用約束
- 離屏渲染
- 畫素混合
PS:因為我們的IM系統是我們自己寫的,中間又經歷了公司分家,人員換了好幾茬,於是就導致了在本來架構不合理的基礎上,實現業務和功能的程式碼更是屎一樣,所以IM的問題更嚴重。而且我們已經計劃了對於IM的重構時間表,所以我會在另一篇Blog裡寫一下我的重構思路。
方案整理
影響流暢度的主要原因:
1、文字寬高計算、檢視佈局計算 2、文字渲染、圖片解碼、圖形繪製 3、物件建立、物件調整、物件銷燬
CPU資源消耗原因以及解決辦法:
1、物件的建立:
物件的建立會分配記憶體、設定屬性等,會消耗CPU資源。所以儘量使用輕量物件代替,比如能用CALayer的時候儘量不用UIView,敏感位置能不用IB儘量使用純程式碼手寫。
推遲同一時間建立物件,推薦使用懶載入在需要使用時候建立物件。
2、物件調整
對 UIView 的這些屬性進行調整時,消耗的資源要遠大於一般的屬性。對此你在應用中,應該儘量減少不必要的屬性修改。
當檢視層次調整時,UIView、CALayer 之間會出現很多方法呼叫與通知,所以在優化效能時,應該儘量避免調整檢視層次、新增和移除檢視。
3、物件銷燬
當前類持有大量物件時候,其銷燬時候的資源消耗就非常明顯。建議建立銷燬的非同步佇列,將需要銷燬的物件放到佇列中銷燬。
4、佈局計算
佈局計算在UITableView使用中是最常見的消耗資源的地方。建議取到資料之後,非同步進行計算佈局並快取下來,當複用Cell時候直接呼叫快取資料。
5、AutoLayout
Autolayout 對於複雜檢視來說常常會產生嚴重的效能問題,AutoLayout相對低效的原因是隱藏在底層的命名為”Cassowary“的約束求解系統,隨著檢視數量的增長,Autolayout 帶來的 CPU 消耗會呈指數級上升,當Cell內約束超過25個的時候,會降低滑動的幀率。
具體:pilky.me/36/。
6、文字的計算以及渲染
UI中存在大量的對於文字高度的適配,可以參考:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文字寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪製文字。儘管這兩個方法效能不錯,但仍舊需要放到後臺執行緒進行以避免阻塞主執行緒。
常見的文字控制元件 (UILabel、UITextView 等),其排版和繪製都是在主執行緒進行的,當顯示大量文字時,CPU 的壓力會非常大。解決辦法是利用TextKit或者是CoreText自定義文字控制元件。詳見:YYText。
7、圖片解碼以及影像的繪製
當你用 UIImage 或 CGImageSource 的那幾個方法建立圖片時,圖片資料並不會立刻解碼。圖片設定到 UIImageView 或者 CALayer.contents 中去,並且 CALayer 被提交到 GPU 前,CGImage 中的資料才會得到解碼。這一步是發生在主執行緒的,並且不可避免。如果想要繞開這個機制,常見的做法是在後臺執行緒先把圖片繪製到 CGBitmapContext 中,然後從 Bitmap 直接建立圖片。目前常見的網路圖片庫都自帶這個功能。
個最常見的地方就是 [UIView drawRect:] 裡面了。由於 CoreGraphic 方法通常都是執行緒安全的,所以影像的繪製可以很容易的放到後臺執行緒進行。
8、檔案系統的呼叫
NSFileManager獲取一個目錄獲取檔案資訊,進行多次遞迴計算,stat幾乎瞬間完成,NSFileManager耗時較長且消耗CPU。
GPU資源消耗原因以及解決辦法:
1、紋理的渲染
當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片並且快速滑動時),CPU 佔用率很低,GPU 佔用非常高,介面仍然會掉幀。避免這種情況的方法只能是儘量減少在短時間內大量圖片的顯示,儘可能將多張圖片合成為一張進行顯示。
2、檢視的混合(Blended)
檢視結構過於複雜,混合的過程、會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當儘量減少檢視數量和層次,並在不透明的檢視裡標明 opaque 屬性以避免無用的 Alpha 通道合成。當然,這也可以用上面的方法,把多個檢視預先渲染為一張圖片來顯示。
- Blended Layers(檢視混合):在同一個區域內,存在著多個有透明度的圖層,那麼GPU需要更多的計算,混合上下多個圖層才能得出最終畫素的RGB值。
- Misaligned Images(畫素對齊):邏輯畫素(point)和 物理畫素(pixel)無法相匹配;圖片的size和顯示圖片的imageView的size(邏輯畫素(point))不相等。
3、圖形的生成
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的向量圖形顯示,通常會觸發離屏渲染(offscreen rendering),而離屏渲染通常發生在 GPU 中。可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉嫁到 CPU 上去。
好的方法是使用圖片遮罩等方法,避免使用圓角和隱形等。詳細:iOS的離屏渲染
解決方案:
1、預先計算UI佈局
獲取資料之後,非同步計算Cell高度以及各控制元件高度和位置,並儲存在CellLayouModel中,當每次Cell需要高度以及內部佈局的時候就可以直接呼叫,不需要進行重複計算。
2、使用自動快取高度
iOS 8之後出現了UITableView通過約束自動計算高度,但是因為iOS對於約束的演算法問題,會導致流暢性降低,FDTemplateLayoutCell很好的優化了這個問題。
3、非同步繪製
Facebook的開源專案Texture(原AsyncDisplayKit),通過利用ASDisplayNode封裝了CALayer,實現了非同步繪製。
第三方微部落格戶端墨客的是現實,當滑動時,鬆開手指後,立刻計算出滑動停止時 Cell 的位置,並預先繪製那個位置附近的幾個 Cell,而忽略當前滑動中的 Cell。但也有缺點,快速滑動的時候有可能會出現大量空白。
3、高效圖片載入
4、預載入
列表當中,當滑動到一個可以設定的位置的時候,提前獲取下載下一頁的資料,並繪製UI。詳見:zhuanlan.zhihu.com/p/23418800。
5、針對Blended Layers以及Misaligned Images
Blended Layers:
- 儘量少的使用具有透明色的圖片
- 儘量多的將UI部件增加背景色
- 減少同一畫素點進行過多的顏色計算
Misaligned Images:
現象:
洋紅色:UIView的frame畫素不對齊,即不能換算成整數畫素值。 黃色:UIImageView的圖片畫素大小與其frame.size不對齊,圖片發生了縮放造成。
解決:
- 儘量使用ceil(),保證沒有小數的UI繪製
- 儘量不實用0.01f標記UITableView或者UICollectionView的header以及footer
- 網路上獲取的圖片沒有@2x和@3x的區別,需要我們縮放圖片到與UIImageView對應的尺寸,且縮放後的圖片的scale和[UIScreen mainScreen].scale相等,再顯示出來。
其他:
下面的情況或操作會引發離屏渲染:
- 為圖層設定遮罩(layer.mask)
- 將圖層的layer.masksToBounds / view.clipsToBounds屬性設定為true
- 將圖層layer.allowsGroupOpacity屬性設定為YES和layer.opacity小於1.0
- 為圖層設定陰影(layer.shadow *)。
- 為圖層設定layer.shouldRasterize=true
- 具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的圖層
- 文字(任何種類,包括UILabel,CATextLayer,Core Text等)。
- 使用CGContext在drawRect :方法中繪製大部分情況下會導致離屏渲染,甚至僅僅是一個空的實現。
開工
我們綜合分析了下現有首頁程式碼的程式碼結構,發現主要存在問題如下
- 較多的使用約束進行佈局
- 每次Cell滑動進入螢幕都需要根據DataSource計算一次Cell高度,浪費時間。
- 畫素混合等問題嚴重。
- 存在離屏渲染。
- 無預載入邏輯,導致滑動流暢性降低。
於是我們做了以下措施:
- 使用frame佈局來替換約束佈局。
- 每次拉取到資料之後,非同步計算Cell高度並做為單獨欄位快取在DataSource中。
- 按照程式碼規範,規避畫素混合問題和離屏渲染。
- 在相應位置增加了預先拉取資料的邏輯,實現效果是使用者沒有將UI滑動到最後一條的時候,資料以及完成了拉取並重新整理UI的邏輯。
總結
效能優化這個東西其實很難形成一個具體的方案,為什麼這麼說?因為之所以稱之為優化,是因為要在原有的程式碼基礎上進行優化,原有的程式碼又有各式各樣的原因導致需要依照現有程式碼來優化,而很難完全脫離現有的情況完全參照某一種的既定方案進行優化。假如說是完全參照某一種的方案優化的話,建議還是將某一個效能敏感的頁面利用Texture進行完全重寫,這樣才能算是整體化一的利用了某一種方案。
Refrence
- 如何做優化,UITabelView才能更加順滑
- iOS 保持介面流暢的技巧
- 優化UITableViewCell高度計算的那些事
- iOS優化(三)沒錯我還是滑動優化
- iOS的離屏渲染
- iOS開發針對對Masonry下的FPS優化討論