Swift-圖像的性能優化

679322754 7年前發布 | 33K 次閱讀 性能優化 Swift Apple 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(柵格化) 來優化

好處:圖像提前生成

壞處: CPUGPU 會頻繁的切換,會導致 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

 

 本文由用戶 679322754 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!