作者:bestswifter
在使用UIKit的過程中,性能優化是永恆的話題。很多人都看過分析優化滑動性能的文章,但其中不少文章只介紹了優化方法卻對背後的原理避而不談,或者是晦澀難懂而且讀者缺乏實踐體驗的機會。不妨思考一下下面的問題自己是否有一個清晰的認識:
為什麼要把控件盡量設置成不透明的,如果是透明的會有什麼影響,如何檢測這種影響?
為什麼cell中的圖片,盡可能要使用正確的大小、格式,如果錯誤會有什麼影響,如何檢測這種影響?
為什麼設置陰影和圓角有可能影響滑動時流暢度?
shouldRasterize和離屏渲染的關系是什麼,何時應該使用?
本文會結合Instrument分析影響性能的因素,提出優化方案並解釋背後的原理,項目初始demo的下載地址在我的Github,強烈建議每一位讀者下載下來隨著我一步一步調試、優化。如果覺得對自己有幫助,可以給一個Star表示支持。後面的圖片較多,流量黨慎入。
基本概念
打開項目後,只要CustomTableCell.swift文件即可,它實現了自定義的UITableViewCell以及內部的UI布局,因為重點在於性能優化,代碼實現的就比較隨意。
首先按下Command + I打開Instrument,本文主要用到的是Core Animation工具:
打開Core Animation調試
注意這個調試必須使用真機,點擊左上角的紅色圓圈就會開始錄制。新手可能不太熟悉,這裡簡單介紹一下調試界面:
調試界面
我們需要了解兩個兩個區域:
這裡記錄了實時的fps數值,有些地方是0是因為屏幕沒有滑動
這是重中之重,接下來我會帶大家逐個理解、體驗這些調試選項
有過游戲經驗的人也許對fps這個概念比較熟悉。我們知道任何屏幕總是有一個刷新率,比如iphone推薦的刷新率是60Hz,也就是說GPU每秒鐘刷新屏幕60次,因此兩次刷新之間的間隔為16.67ms。這段時間內屏幕內容保持不變,稱為一幀(frame),fps表示frames per second,也就是每秒鐘顯示多少幀畫面。對於靜止不變的內容,我們不需要考慮它的刷新率,但在執行動畫或滑動時,fps的值直接反映出滑動的流暢程度。
調試、優化
圖層混合
首先我們要明白像素的概念,屏幕上每一個點都是一個像素,像素有R、G、B三種顏色構成(有時候還帶有alpha值)。如果某一塊區域上覆蓋了多個layer,最後的顯示效果受到這些layer的共同影響。舉個例子,上層是藍色(RGB=0,0,1),透明度為50%,下層是紅色(RGB=1,0,0)。那麼最終的顯示效果是紫色(RGB=0.5,0,0.5)。這種顏色的混合(blending)需要消耗一定的GPU資源,因為實際上可能不止只有兩層。如果只想顯示最上層的藍色,可以把它的透明度設置為100%,這樣GPU會忽略下面所有的layer,從而節約了很多不必要的運算。
第一個調試選項"Color Blended Layers"正是用於檢測哪裡發生了圖層混合,並用紅色標記出來。因此我們需要盡可能減少看到的紅色區域。一旦發現應該想法設法消除它。開始調試後勾選這個選項,我們在手機上可以看到如下的場景:
Color Blended Layers
很多文章裡說把控件設置為opaque = true,其原理就是希望避免圖層混合,然而這種調優一般情況下用處不大。因為UIView的opaque屬性默認值就是true,也就是說只要不是人為設置成透明,都不會出現圖層混合。比如demo中就沒有任何透明的控件。
對於UIImageView來說,不僅它自身需要是不透明的,它的圖片也不能含有alpha通道,這就是為什麼圖中第三個圖片是綠色,而前兩個圖片是紅色的原因。由於本人對PS和圖像幾乎一竅不通,恕我不能演示如何消除這些圖片的紅色。我從網上找了一個美女的頭像來說明,圖像自身的性質可能會對結果有影響,因此如果你確定自己的代碼沒有問題,而且出現了圖層混合,請聯系美工或後台解決。
個人認為比opaque屬性更重要的是backgroundColor屬性,如果不設置這個屬性,控件依然被認為是透明的,所以我們做的第一個優化是在CustomTableCell類的init方法中添加一行代碼:
label.backgroundColor = UIColor.whiteColor()
雖然在白色背景下,這行代碼無法肉眼看到效果,但重新調試後我們可以發現label的紅色消失了。也正是因為對背景顏色的不重視,它成了影響滑動性能的第一個殺手。
PS:如果label文字有中文,依然會出現圖層混合,這是因為此時label多了一個sublayer,如果有好的解決辦法歡迎告訴我。
光柵化
光柵化是將一個layer預先渲染成位圖(bitmap),然後加入緩存中。如果對於陰影效果這樣比較消耗資源的靜態內容進行緩存,可以得到一定幅度的性能提升。demo中的這一行代碼表示將label的layer光柵化:
label.layer.shouldRasterize = true
Instrument中,第二個調試選項是“Color Hits Green and Misses Red”,它表示如果命中緩存則顯示為綠色,否則顯示為紅色,顯然綠色越多越好,紅色越少越好。勾選這個選項後我們看到如下的場景:
Color Hits Green and Misses Red
光柵化的核心在於緩存的思想。我們自己動手把玩一下,可以發現以下幾個有意思的現象:
上下微小幅度滑動時,一直是綠色
上下較大幅度滑動,新出現的label一開始是紅色,隨後變成綠色
如果靜止一秒鐘,剛開始滑動時會變紅。
這是因為layer進行光柵化後渲染成位圖放在緩存中。當屏幕出現滑動時,我們直接從緩存中讀取而不必渲染,所以會看到綠色。當新的label出現時,緩存中沒有個這個label的位圖,所以會變成紅色。第三點比較關鍵,緩存中的對象有效期只有100ms,即如果在0.1s內沒有被使用就會自動從緩存中清理出去。這就是為什麼停留一會兒再滑動就會看到紅色。
光柵化的緩存機制是一把雙刃劍,先寫入緩存再讀取有可能消耗較多的時間。因此光柵化僅適用於較復雜的、靜態的效果。通過Instrument的調試發現,這裡使用光柵化經常出現未命中緩存的情況,如果沒有特殊需要則可以關閉光柵化,所以我們做的第二個優化是注釋掉下面這行代碼:
// label.layer.shouldRasterize = true
光柵化會導致離屏渲染,這一點待會兒會講。
顏色格式
像素在內存中的布局和它在磁盤中的存儲方式並不相同。考慮一種簡單的情況:每個像素有R、G、B和alpha四個值,每個值占用1字節,因此每個像素占用4字節的內存空間。一張1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600個像素,因此占用了超過8Mb的內存。但是一張同樣分辨率的PNG格式或JPEG格式的圖片一般情況下不會有這麼大。這是因為JPEG將像素數據進行了一種非常復雜且可逆的轉化。
當我們打開JPEG格式的圖片時,CPU會進行一系列運算,將JPEG圖片解壓成像素數據。顯然這個工作會消耗不少時間,所以不應該在滑動時進行,我們應該預先處理好圖片。借用WWDC上的一頁PPT來說明:
顯示流程
Commit Transaction和Decode在同一幀內進行,如果這兩個操作的耗時超過16.67s,Draw Calls就會延遲到下一幀,從而導致fps值的降低。下面是Commit Transaction的詳細流程:
解碼與轉換
在第三步的Prepare中,CPU主要處理兩件事:
把圖片從PNG或JPEG等格式中解壓出來,得到像素數據。
如果GPU不支持這種顏色各式,CPU需要進行格式轉換。
比如應用中有一些從網絡下載的圖片,而GPU恰好不支持這個格式,這就需要CPU預先進行格式轉化。第三個選項“Color Copied Images”就用來檢測這種實時的格式轉化,如果有則會將圖片標記為藍色。
遺憾的是由於我對圖片格式不太了解,也不會使用相關工具,並沒有能模擬出觸發這個選項的場景。我們要記住的是,如果調試時發現有圖片被標記為藍色,說明圖片格式出現了一些問題。
圖片大小
第四個選項的使用場景不多,我們直接看一下第五個選項“Color Misaligned Images”。它表示如果圖片需要縮放則標記為黃色,如果沒有像素對齊則標記為紫色。勾選上這個選項並進行調試,可以看到如下場景:
圖片縮放
在demo中,每個UIImageView的大小都是180x180,而只有第二張圖片的像素大小是360x360。因此除了第二張圖片,其他的圖片都需要被縮放。圖片的縮放需要占用時間,因此我們要盡可能保證無論是本地圖片還是從網絡或取得圖片的大小,都與其frame保持一致。
第三個優化是調整所有圖片的像素大小以避免不必要的縮放。
離屏渲染
離屏渲染表示渲染發生在屏幕之外,你可能認為這是一句廢話。為了真正解釋清楚什麼是離屏渲染,我們先來看一下正常的渲染通道(Render-Pass):
正常渲染通道
首先,OpenGL提交一個命令到Command Buffer,隨後GPU開始渲染,渲染結果放到Render Buffer中,這是正常的渲染流程。但是有一些復雜的效果無法直接渲染出結果,它需要分步渲染最後再組合起來,比如添加一個蒙版(mask):
離屏渲染
在前兩個渲染通道中,GPU分別得到了紋理(texture,也就是那個相機圖標)和layer(藍色的蒙版)的渲染結果。但這兩個渲染結果沒有直接放入Render Buffer中,也就表示這是離屏渲染。直到第三個渲染通道,才把兩者組合起來放入Render Buffer中。離屏渲染意味著把渲染結果臨時保存,等用到時再取出,因此相對於普通渲染更占用資源。
第六個選項“Color Offscreen-Rendered Yellow”會把需要離屏渲染的地方標記為黃色,大部分情況下我們需要盡可能避免黃色的出現。離屏渲染可能會自動觸發,也可以手動觸發。以下情況可能會導致觸發離屏渲染:
重寫drawRect方法
有mask或者是陰影(layer.masksToBounds, layer.shadow*),模糊效果也是一種mask
layer.shouldRasterize = true
前兩者會自動觸發離屏渲染,第三種方法是手動開啟離屏渲染。
開始調試並勾選“Color Offscreen-Rendered Yellow”,會看到這樣的場景:
離屏渲染
如果沒有進行第二步優化,你會發現label也是黃色。可以看到tabbar和statusBar也是黃色,這是因為它們使用了模糊效果。圖片也是黃色,這說明它也進行了離屏渲染,觀察源碼後發現主要原因是它使用了陰影,接下來我們進行第四個優化,在設置陰影效果的四行代碼下面添加一行:
imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
這行代碼制定了陰影路徑,如果沒有手動指定,Core Animation會去自動計算,這就會觸發離屏渲染。如果人為指定了陰影路徑,就可以免去計算,從而避免產生離屏渲染。
設置cornerRadius本身並不會導致離屏渲染,但很多時候它還需要配合layer.masksToBounds = true使用。根據之前的總結,設置masksToBounds會導致離屏渲染。解決方案是盡可能在滑動時避免設置圓角,如果必須設置圓角,可以使用光柵化技術將圓角緩存起來:
// 設置圓角 label.layer.masksToBounds = true label.layer.cornerRadius = 8 label.layer.shouldRasterize = true label.layer.rasterizationScale = layer.contentsScale
快速路徑
還記得之前將離屏渲染和渲染路徑時的示意圖麼,離屏渲染的最後一步是把此前的多個路徑組合起來。如果這個組合過程能由CPU完成,就會大量減少GPU的工作。這種技術在繪制地圖中可能用到。
第七個選項“Color Compositing Fast-Path Blue”用於標記由硬件繪制的路徑,藍色越多越好。
變化區域
刷新視圖時,我們應該把需要重繪的區域盡可能縮小。對於未發生變化的內容則不應該重繪,第八個選項“Flash updated Regions”用於標記發生重繪的區域。一個典型的例子是系統的時鐘應用,絕大多數時候只有顯示秒針的區域需要重繪:
重繪區域
總結
如果你一步一步做到了這裡,我想一定會有不少收益。不過,學而不思則罔,思而不學則殆。動手實踐後還是應該總結提煉,優化滑動性能主要涉及三個方面:
避免圖層混合
確保控件的opaque屬性設置為true,確保backgroundColor和父視圖顏色一致且不透明。
如無特殊需要,不要設置低於1的alpha值。
確保UIImage沒有alpha通道。
避免臨時轉換
確保圖片大小和frame一致,不要在滑動時縮放圖片。
確保圖片顏色格式被GPU支持,避免勞煩CPU轉換。
慎用離屏渲染
絕大多數時候離屏渲染會影響性能。
重寫drawRect方法,設置圓角、陰影、模糊效果,光柵化都會導致離屏渲染。
設置陰影效果是加上陰影路徑。
滑動時若需要圓角效果,開啟光柵化。
實戰
本文的demo可以在我的Github上下載,然後一步一步自己體驗優化過程。但demo畢竟是刻意搭建的一個環境,我會在我自己的仿寫的簡書app上不斷進行實戰優化,歡迎共同學習交流。
參考資料:
繪制像素到屏幕上,原文:Getting Pixels onto the Screen
Advanced Graphics and Animations for iOS Apps:這是2014年WWDC Session 419,強烈建議看一遍。
如何正確地寫好一個界面
Mastering UIKit Performance
還有一些高質量的問答:
What triggers “Color Copied Images” and “Color Hits Green and Misses Red” in Instruments?
UILabel is marked as red when Color Blended Layers is selected
What triggers offscreen rendering, blending and layoutSubviews in iOS?