Swift-影象的效能優化

宮城良田發表於2017-08-28

前言

隨著移動端的發展,現在越來越注重效能優化了。這篇文章將談一談對於圖片的效能優化。面試中又會經常有這樣的問題:如何實現一個影象的圓角,不要用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)複製程式碼

圖片在模擬器上的顯示

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

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

事實證明,如果影象尺寸和ImageView尺寸不一致,影象就一定會被拉伸,只要被拉伸,CPU就會工作,如果是在cell上,每次cell離開螢幕再回到螢幕的時候,都會對圖片進行拉伸處理。就會頻繁的消耗CPU從而導致影響APP的效能。

Color Offscreen-Rendered(離屏渲染->有待完善)

  • 這裡會把那些需要離屏渲染的圖層高亮成黃色
  • 這些圖層很可能需要用shadownPath或者shouldRasterize(柵格化)來優化

好處:影象提前生成

壞處:CPUGPU會頻繁的切換,會導致CPU的消耗會高一點,但是效能會提升

小結:

  • 以上效能優化中,有效的檢測Color Blended LayersColor 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)

}複製程式碼

自定義建立影象的方法

/// 將給定的影象進行拉伸,並且返回新的影象
///
/// - 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
}複製程式碼

效果如下

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

再解決混合模式(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
}複製程式碼

封裝

為了方便自己以後用,因此,將其封裝起來。如果有更好的改進辦法歡迎給我提出。

建立了一個空白檔案HQImage,在UIImageextension裡面自定義了兩個方法建立頭像影象(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
    }
}複製程式碼

效能測試

沒有對比就無從談起效能優化,以下是我根據兩種方法,迴圈建立100ImageViewCPU記憶體消耗(個人感覺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)
}複製程式碼

自定義方法建立影象

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)
}複製程式碼

由此可見,新方法對CPU消耗明顯減少,記憶體較以前稍微上漲,CPU消耗減少,則效能有所提升。(因為每次消耗不是一個定數,我這裡也是測了很多次取的大概的平均值。)


2017年08月30日補充

感謝linbx08給我提出的問題,是一個關於矩形影象呼叫我的方法hq_rectImage影象右側顯示黑線的問題。

解決辦法是在開啟圖形上下文後,對其做背景填充。

// 背景填充(在裁切之前做填充)
backColor.setFill()
UIRectFill(rect)複製程式碼

但黑線的原因暫時尚未查明。我之前的思路是按照做圓形頭像的程式碼繼續做的。直接UIBezierPath(rect: rect)例項化了一個矩形的路徑,然後在路徑內繪圖。但是突然想到不用裁切,不用設定圓形頭像的邊框,突然感覺這樣就有點多此一舉了,因此將多餘的程式碼就都刪除了。沒想到刪多了,出問題了,不過好在有人及時給我提出了問題。並幫助我改正、再次感謝!


2017年09月04日補充

又發現一個問題

就是如果按照最之前寫的程式碼,在設定矩形圖片時,如果不在開啟圖形上下文後,對背景做填充,那麼當你的影象不是一個矩形的時候(是任意的不規則形狀),那麼,背景被填充的是黑色,在你的圖形以外的範圍內會被看見。如下圖

看下我寫的程式碼

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: CGSize(width: 50, height: 50))
        }
    }複製程式碼

解決辦法同之前的方式,開啟圖形上下文後,填充背景色就OK了。

DEMO傳送門 : ImagePerformanceOptimization

歡迎來我的簡書看看 : 紅鯉魚與綠鯉魚與驢___

相關文章