Swift-圖像的性能優化
前言
隨著移動端的發展,現在越來越注重性能優化了。這篇文章將談一談對于圖片的性能優化。面試中又會經常有這樣的問題:如何實現一個圖像的圓角,不要用 cornerRadius ?
模擬器常用性能測試工具
Color Blended Layers(混合圖層->檢測圖像的混合模式)
- 此功能基于渲染程度對屏幕中的混合區域進行 綠->紅 的高亮(也就是多個半透明層的疊加,其中綠色代表比較好,紅色則代表比較糟糕)
- 由于重繪的原因,混合對 GPU ( Graphics Processing Unit->專門用來畫圖的 )性能會有影響,同時也是滑動或者動畫幀率下降的罪魁禍首之一
GPU:如果有透明的圖片疊加,做兩個圖像透明度之間疊加的運算,運算之后生成一個結果,顯示到屏幕上,如果透明的圖片疊加的很多,運算量就會很大
png 格式的圖片是透明的,如果邊上有無色的地方,那么可以把底下的背景透過來
一般指定顏色的時候不建議使用透明色,透明色執行效率低
Color Copied Images(圖像復制->幾乎用不到)
- 有時候 寄宿圖片(layer.content) 的生成是由 Core Animation 被強制生成一些圖片,然后發送到渲染服務器,而不是簡單的指向原始指針
- 這個選項把這些圖片渲染成藍色
- 復制圖片對內存和 CPU 使用來說都是一項非常昂貴的操作,所以應該盡可能的避免
Color Misaligned Images(拉伸圖像->檢測圖片有沒有被拉伸)
- 會高亮那些被縮放或者拉伸以及沒有正確對齊到像素邊界的圖片(也就是非整型坐標)
- 通常都會導致圖片的不正常縮放,比如把一張大圖當縮略圖顯示,或者不正確的模糊圖像
如果圖片做 拉伸 的動作,是消耗 CPU 的。如果圖片顯示在一個 Cell 上面,滾出屏幕再滾動回來的時候,圖片仍然需要重新被設置,在進入屏幕之前還需要一次 拉伸操作 ,這些 拉伸 的操作是會消耗 CPU 的計算的。這樣的設置多了以后就會嚴重影響性能。一個圖片是否被進行了 拉伸操作 ,我們用模擬器就可以判斷出來。
為什么我們說這種方法設置圖像效果不好
Color Misaligned Images(拉伸圖像->檢測圖片有沒有被拉伸)
創建一個自定義尺寸的 ImageView ,并設置圖像
let image = UIImage(named: "avatar_default")
let imageView01 = UIImageView(frame: CGRect(x: 100, y: 100, width: 160, height: 160))
imageView01.image = image
view.addSubview(imageView01)</code></pre>

圖片在模擬器上的顯示

利用模擬器的 Debug 的 Color Misaligned Images 功能查看圖片狀態。如下圖所示,圖片顯示黃色,證明圖片被拉伸了。

就知道你可能會不相信,繼續看!將 ImageView 的尺寸設置成和圖片一樣大小,再利用模擬器 Color Misaligned Images 功能再次查看圖片狀態。結果如圖所示

事實證明,如果圖像尺寸和 ImageView 尺寸不一致,圖像就一定會被拉伸,只要被拉伸, CPU 就會工作,如果是在 cell 上,每次 cell 離開屏幕再回到屏幕的時候,都會對圖片進行拉伸處理。就會頻繁的消耗 CPU 從而導致影響 APP 的性能。
Color Offscreen-Rendered(離屏渲染->有待完善)
- 這里會把那些需要離屏渲染的圖層高亮成黃色
- 這些圖層很可能需要用 shadownPath 或者 shouldRasterize(柵格化) 來優化
好處:圖像提前生成
壞處: CPU 和 GPU 會頻繁的切換,會導致 CPU 的消耗會高一點,但是性能會提升
小結:
- 以上性能優化中,有效的檢測 Color Blended Layers 和 Color Misaligned Images 在開發中能夠提升圖像的性能
- Color Copied Images 幾乎遇不到
- Color Offscreen-Rendered 主要用于 cell 的性能優化
解決圖片拉伸問題
利用核心繪圖功能實現,根據尺寸獲取路徑,重新繪制一個目標尺寸的圖片
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "avatar_default")
let imageView01 = UIImageView(frame: CGRect(x: 100, y: 100, width: 160, height: 160))
imageView01.image = image
view.addSubview(imageView01)
let rect = CGRect(x: 100, y: 300, width: 160, height: 160)
let imageView02 = UIImageView(frame: rect)
// 自定義創建圖像的方法
imageView02.image = avatarImage(image: image!, size: rect.size)
view.addSubview(imageView02)
}</code></pre>
自定義創建圖像的方法
/// 將給定的圖像進行拉伸,并且返回新的圖像
///
/// - Parameters:
/// - image: 原圖
/// - size: 目標尺寸
/// - Returns: 返回一個新的'目標尺寸'的圖像
func avatarImage(image: UIImage, size: CGSize) -> UIImage? {
let rect = CGRect(origin: CGPoint(), size: size)
// 1.圖像的上下文-內存中開辟一個地址,跟屏幕無關
/**
* 1.繪圖的尺寸
* 2.不透明:false(透明) / true(不透明)
* 3.scale:屏幕分辨率,默認情況下生成的圖像使用'1.0'的分辨率,圖像質量不好
* 可以指定'0',會選擇當前設備的屏幕分辨率
*/
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
// 2.繪圖'drawInRect'就是在指定區域內拉伸屏幕
image.draw(in: rect)
// 3.取得結果
let result = UIGraphicsGetImageFromCurrentImageContext()
// 4.關閉上下文
UIGraphicsEndImageContext()
// 5.返回結果
return result
}</code></pre>
效果如下

如果到這里你以為就完事了,那你真是太年輕了

再解決混合模式 (Color Blended Layers) 問題
繼續剛才的話題,僅僅解決了拉伸問題后,在 Color Blended Layers(混合模式) 下還是有問題,如圖

將繪圖選項的透明狀態設置為 不透明(true)

到這里,如果類似新聞 APP 圖片都只是顯示方形的,就可以搞定了。那如果是頭像怎么辦呢?頭像絕大多數都是圓角頭像,而且現在越來越多的考慮到性能方面的問題。很多人都不用 cornerRadius ,認為用 cornerRadius 不是一個好的解決辦法。
設置圖像圓角,不用 cornerRadius
在 獲取上下文(UIGraphicsBeginImageContextWithOptions) 和 繪圖(drawInRect) 之間實例化一個圓形的路徑,并進行路徑裁切
// 1> 實例化一個圓形的路徑
let path = UIBezierPath(ovalIn: rect)
// 2> 進行路徑裁切 - 后續的繪圖,都會出現在圓形路徑內部,外部的全部干掉
path.addClip()
效果如下

UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) 這里選擇了 true(不透明) ,四個角即使被裁切掉(沒有在獲取到的路徑里面)但是由于是 不透明 的模式,所以看不到下面的顏色,默認看到了黑色的背景。
將 UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) 透明模式改為 false(透明)

再看下混合模式,四個叫和頭像都是紅色,并且顏色深淺程度不一樣,越紅效率越不好。證明有圖層疊加的運算,因此,不能采用透明的模式。

解決辦法:給背景設置一個顏色,使其不顯示默認的黑色。
這樣就可以解決四個角顯示黑色的問題,并且在混合模式狀態下不會再有紅色顯示,性能可以非常的好。

開發過程中,用顏色比用圖片性能會高一點。
不到萬不得已, View 的背景色盡量不要設置成透明顏色。
給圖像添加邊框,繪制內切的圓形
UIColor.darkGray.setStroke()
path.lineWidth = 5 // 默認是'1'
path.stroke()

判斷一個應用程序的好壞,看圖像處理的是否到位,如果表格里面圖像都拉伸,并且設置 cornerRadius ,那么表格的卡頓可能將會變得非常明顯。
下面是方法的最終代碼:
/// 將給定的圖像進行拉伸,并且返回新的圖像
///
/// - Parameters:
/// - image: 原圖
/// - size: 目標尺寸
/// - Returns: 返回一個新的'目標尺寸'的圖像
func avatarImage(image: UIImage, size: CGSize, backColor:UIColor?) -> UIImage? {
let rect = CGRect(origin: CGPoint(), size: size)
// 1.圖像的上下文-內存中開辟一個地址,跟屏幕無關
/**
* 1.繪圖的尺寸
* 2.不透明:false(透明) / true(不透明)
* 3.scale:屏幕分辨率,默認情況下生成的圖像使用'1.0'的分辨率,圖像質量不好
* 可以指定'0',會選擇當前設備的屏幕分辨率
*/
UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
// 背景填充(在裁切之前做填充)
backColor?.setFill()
UIRectFill(rect)
// 1> 實例化一個圓形的路徑
let path = UIBezierPath(ovalIn: rect)
// 2> 進行路徑裁切 - 后續的繪圖,都會出現在圓形路徑內部,外部的全部干掉
path.addClip()
// 2.繪圖'drawInRect'就是在指定區域內拉伸屏幕
image.draw(in: rect)
// 3.繪制內切的圓形
UIColor.darkGray.setStroke()
path.lineWidth = 5 // 默認是'1'
path.stroke()
// 4.取得結果
let result = UIGraphicsGetImageFromCurrentImageContext()
// 5.關閉上下文
UIGraphicsEndImageContext()
// 6.返回結果
return result
}</code></pre>
封裝
為了方便自己以后用,因此,將其封裝起來。如果有更好的改進辦法歡迎給我提出。
建立了一個空白文件 HQImage ,在 UIImage 的 extension 里面自定義了兩個方法 創建頭像圖像(hq_avatarImage) 和 創建矩形圖像(hq_rectImage)
// MARK: - 創建圖像的自定義方法
extension UIImage {
/// 創建圓角圖像
///
/// - Parameters:
/// - size: 尺寸
/// - backColor: 背景色(默認`white`)
/// - lineColor: 線的顏色(默認`lightGray`)
/// - Returns: 裁切后的圖像
func hq_avatarImage(size: CGSize?, backColor: UIColor = UIColor.white, lineColor: UIColor = UIColor.lightGray) -> UIImage? {
var size = size
if size == nil {
size = self.size
}
let rect = CGRect(origin: CGPoint(), size: size!)
// 1.圖像的上下文-內存中開辟一個地址,跟屏幕無關
/**
* 1.繪圖的尺寸
* 2.不透明:false(透明) / true(不透明)
* 3.scale:屏幕分辨率,默認情況下生成的圖像使用'1.0'的分辨率,圖像質量不好
* 可以指定'0',會選擇當前設備的屏幕分辨率
*/
UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
// 背景填充(在裁切之前做填充)
backColor.setFill()
UIRectFill(rect)
// 1> 實例化一個圓形的路徑
let path = UIBezierPath(ovalIn: rect)
// 2> 進行路徑裁切 - 后續的繪圖,都會出現在圓形路徑內部,外部的全部干掉
path.addClip()
// 2.繪圖'drawInRect'就是在指定區域內拉伸屏幕
draw(in: rect)
// 3.繪制內切的圓形
UIColor.darkGray.setStroke()
path.lineWidth = 1 // 默認是'1'
path.stroke()
// 4.取得結果
let result = UIGraphicsGetImageFromCurrentImageContext()
// 5.關閉上下文
UIGraphicsEndImageContext()
// 6.返回結果
return result
}
/// 創建矩形圖像
///
/// - Parameters:
/// - size: 尺寸
/// - backColor: 背景色(默認`white`)
/// - lineColor: 線的顏色(默認`lightGray`)
/// - Returns: 裁切后的圖像
func hq_rectImage(size: CGSize?, backColor: UIColor = UIColor.white, lineColor: UIColor = UIColor.lightGray) -> UIImage? {
var size = size
if size == nil {
size = self.size
}
let rect = CGRect(origin: CGPoint(), size: size!)
// 1.圖像的上下文-內存中開辟一個地址,跟屏幕無關
/**
* 1.繪圖的尺寸
* 2.不透明:false(透明) / true(不透明)
* 3.scale:屏幕分辨率,默認情況下生成的圖像使用'1.0'的分辨率,圖像質量不好
* 可以指定'0',會選擇當前設備的屏幕分辨率
*/
UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
// 2.繪圖'drawInRect'就是在指定區域內拉伸屏幕
draw(in: rect)
// 3.取得結果
let result = UIGraphicsGetImageFromCurrentImageContext()
// 4.關閉上下文
UIGraphicsEndImageContext()
// 5.返回結果
return result
}
}</code></pre>
性能測試
沒有對比就無從談起性能優化,以下是我根據兩種方法,循環創建 100 個 ImageView 的 CPU 和 內存 消耗(個人感覺 1 張圖片不一定能說明問題,所以搞了 100 個)
系統方法創建圖像
for _ in 0..<100 {
let imageView01 = UIImageView(frame: CGRect(x: 100, y: 100, width: 160, height: 160))
imageView01.image = image
view.addSubview(imageView01)
}</code></pre>

自定義方法創建圖像
for _ in 0..<100 {
let rect02 = CGRect(x: 100, y: 300, width: 160, height: 160)
let imageView02 = UIImageView(frame: rect02)
imageView02.image = avatarImage(image: image!, size: rect02.size, backColor: view.backgroundColor)
view.addSubview(imageView02)
}</code></pre>

由此可見,新方法對CPU消耗明顯減少,內存較以前稍微上漲,CPU消耗減少,則性能有所提升。(因為每次消耗不是一個定數,我這里也是測了很多次取的大概的平均值。)
來自:https://juejin.im/post/59a3e67df265da2473444281