iOS - 圖片使用優(yōu)化的一些總結(jié)

最近在使用動圖時(shí)

open class func animatedImage(with images: [UIImage], duration: TimeInterval) -> UIImage?

發(fā)現(xiàn)一個(gè)現(xiàn)象,因?yàn)?animatedImage 需要加載大量 image,所以在加載時(shí)會造成一定的卡頓。因此我想通過一些測試記錄一下 image 在使用過程中的一些優(yōu)化

1. 圖片對包大小的影響

所有圖片用同一張圖片,大小為 3840 x 2160

圖片格式 圖片大小 圖片放Bundle 包大小 圖片放 Assets 包大小 備注
png 22.2 MB 24.5 MB 16.2 MB
png 5.6 MB 6.3 MB 6.1 MB 關(guān)閉 Compress PNG Files - Packaging ,包大小為 5.8 MB,原因
jpg 8.3 MB 8.4 MB 8.4 MB
jpg 752 KB 912 KB 932 KB

1.1 結(jié)論

  1. 圖片大小對包大小是有一定影響的,最好對工程中所有圖片進(jìn)行壓縮
  2. png 圖片放在 Assets 中,iOS 會對其進(jìn)行進(jìn)一步的壓縮,可以考慮使用 Assets 管理 png 圖片
  3. Building Setting 中的 Compress PNG Files - Packaging 選項(xiàng)關(guān)閉可以稍微減少包大小,但是會影響 png 在使用時(shí)加載的速度,所以建議保持默認(rèn)值,開啟

2. UIImage 的 init 方法對比

  1. 方法一
public init?(named name: String)

This method checks the system caches for an image object with the specified name and returns the variant of that image that is best suited for the main screen. If a matching image object is not already in the cache, this method creates the image from an available asset catalog or loads it from disk. The system may purge cached image data at any time to free up memory. Purging occurs only for images that are in the cache but are not currently being used.

此方法會從系統(tǒng)緩存檢查是否具有指定名稱的 UIImage 對象。如果緩存中不存在匹配的圖像對象,則此方法將從 assets 目錄創(chuàng)建圖像或從磁盤加載圖像。系統(tǒng)可以隨時(shí)清除緩存的圖像數(shù)據(jù)以釋放內(nèi)存。僅對緩存中但當(dāng)前未使用的圖像進(jìn)行清除。

  1. 方法二
public init?(contentsOfFile path: String)

This method loads the image data into memory and marks it as purgeable. If the data is purged and needs to be reloaded, the image object loads that data again from the specified path.

此方法將圖像數(shù)據(jù)加載到內(nèi)存中并將其標(biāo)記為可清除。如果清除數(shù)據(jù)并需要重新加載,則圖像對象將從指定路徑再次加載該數(shù)據(jù)。

2.1 結(jié)論

對于方法兩個(gè)方法的主要區(qū)別就是

  • 方法一:創(chuàng)建的 UIImage 的加載到內(nèi)存中后,會一直存在內(nèi)存中,及時(shí)持有 UIImage 的對象(如 UIImageView)釋放了也不會釋放。以及加載到內(nèi)存中時(shí)就是解碼后的圖片。
  • 方法二:沒有對象(如 UIImageView)持有該 UIImage,會從內(nèi)存中釋放,下次使用時(shí)會從指定的 path 重新加載。在內(nèi)存中不是解碼后的圖片,需要在渲染前額外進(jìn)行解碼操作。

這就造成了兩者的使用場景不同

  • 方法一:頻繁使用的小圖片
  • 方法二:不頻繁使用的圖片,大圖片,不包含在 Bundle 中的圖片

2.2 理解誤區(qū)

同時(shí)這里有一個(gè)我之前一直理解錯(cuò)誤的誤區(qū),因?yàn)橹翱催^各種各樣的資料,說的不盡相同。也是這次自己編寫代碼測試過后才重新確定了一些邏輯。

當(dāng)我們執(zhí)行以下代碼

let image = UIImage(named: "1")

或者

let path = Bundle.main.path(forResource: "1", ofType: "png")!
let image = UIImage(contentsOfFile: path)

此時(shí)只是創(chuàng)建了 UIImage 對象,并不會把圖片加載到內(nèi)存(測試過程中未看到使用內(nèi)存增加)中以及進(jìn)行圖片解碼(未造成卡頓)

只有使用到 UIImage 時(shí),即將 image 賦值給 layer 的 contents 或者 imageView 的時(shí)候,才會加載 image 到內(nèi)存以及解碼,這個(gè)時(shí)候才是一些卡頓發(fā)生的時(shí)間。

我看 iOS Core Animation: Advanced Techniques 在講如何避免延遲解碼帶來的卡頓有這樣一段話,給我?guī)砗芏嗟母蓴_

使用 UIImage+imageNamed: 方法避免延時(shí)加載。不像 +imageWithContentsOfFile:(和其他別的UIImage加載方法),這個(gè)方法會在加載圖片之后立刻進(jìn)行解碼。

我品了品這句話的意思,說的有道理,其實(shí)并沒什么卵用。因?yàn)榧虞d的時(shí)機(jī)都是將 image 賦值給 layer 的 contents,此時(shí)用 +imageNamed: 會解碼完存在內(nèi)存中;+imageWithContentsOfFile 直接存在內(nèi)存中,然后在渲染前解碼。兩者比較其實(shí) +imageNamed: 并沒有把解碼時(shí)機(jī)提前,卡頓時(shí)間應(yīng)該是一樣的,它的優(yōu)勢僅僅在頻繁使用時(shí)只要解碼一次

3. png 、jpg 加載速度測試

測試環(huán)境

  • iPhone7 真機(jī)

  • iOS 13.1

測試不同大小的 png、jpg 圖片加載以及解碼的耗時(shí),圖片越大加載速度越慢,png 往往比 jpg 大,但是 png 解碼速度相比 jpg 又有優(yōu)勢,總耗時(shí)要綜合考慮兩個(gè)因素。圖片都是有 Mac 上自帶的 預(yù)覽 導(dǎo)出的,計(jì)算一百次取平均值

時(shí)間計(jì)算代碼

static func loadImage(contentsOfFile path: String) -> CFAbsoluteTime {
    UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))

    var loadTime: CFAbsoluteTime = 0

    for _ in 0 ..< 100 {
        let tmpTime = CFAbsoluteTimeGetCurrent()
        // load image
        let image = UIImage(contentsOfFile: path)!

        // decompress image by drawing it
        image.draw(at: CGPoint.zero)

        loadTime += CFAbsoluteTimeGetCurrent() - tmpTime
    }

    UIGraphicsEndImageContext()

    return (loadTime / 100)
}

結(jié)果

類型 大小 時(shí)間 / ms
png 3840 x 2160 (22.2 MB) 111.27
png 1920 x 1080 (4.7 MB) 28.75
png 480 x 270 (243 KB) 2.93
png 160 x 90 (30 KB) 1.22
png 48 x 27 (4KB) 0.80
jpg 3840 x 2160(8.3 MB) 32.54
jpg 1920 x 1080(491KB) 13.82
jpg 480 x 270 (39KB) 9.81
jpg 160 x 90 (12 KB) 1.96
jpg 48 x 27 (8KB) 1.89

3.1 結(jié)論

  • 對于大圖,可以使用 jpg,不僅體積小,加載速度快
  • 對于小圖,可以使用 png,此時(shí) png 和 jpg 大小差異不明顯,選擇解碼速度快的

4. animatedImage 加載優(yōu)化

實(shí)現(xiàn)顯示一個(gè) animatedImage 會卡頓的優(yōu)化,測試用的動圖總共 222 張,大小為 500 x 375,總共的大小為 2.8 MB

計(jì)算卡頓的方式使用 CADisplayLink,當(dāng)發(fā)生卡頓時(shí),輸出總共丟失的幀數(shù)

link = CADisplayLink(target: self, selector: #selector(tick(_:)))
link.add(to: RunLoop.main, forMode: RunLoop.Mode.common)

/// 當(dāng)發(fā)生卡頓時(shí),輸出丟失的幀數(shù)
@objc private func tick(_ link: CADisplayLink) {
    if lastTime == 0 {           // 對lastTime進(jìn)行初始化
      lastTime = link.timestamp
      return
    }

    let delta = link.timestamp - self.lastTime;  //計(jì)算本次刷新和上次更新FPS的時(shí)間間隔

    let timeInterval = 1.0 / 60.0

    if delta > timeInterval * 1.5 {
      let lostFrame = delta / timeInterval
      print(String(format: "卡頓的幀數(shù):%.2f", lostFrame))
    }

    self.lastTime = link.timestamp
}

4.1 不做處理

var animatedImage: UIImage = {
    var images: [UIImage] = []
    for i in 0 ... 221 {
      let imagePath = Bundle.main.path(forResource: "\(i)", ofType: "png")!
      let image = UIImage(contentsOfFile: imagePath)!
      images.append(image)
    }

    return UIImage.animatedImage(with: images, duration: 5)!
}()

// 使用
imageView.image = animatedImage

輸出的結(jié)果如下,前面的 16 幀丟失應(yīng)該是加載圖片造成的,后面的 25 幀丟失應(yīng)該是圖片解碼造成的

卡頓的幀數(shù):16.00
卡頓的幀數(shù):25.00

同時(shí)查看使用的內(nèi)存為 17 MB -> 180 MB

4.2 提前一步加載解碼圖片

我們在使用動圖之前提前對圖片進(jìn)行加載和解碼

我們解碼的方法如下

struct Utils {
    /// 使用 CGContext,繪制時(shí)會自動進(jìn)行解碼
    static func decompress(_ imagePath: String, in size: CGSize) -> UIImage? {
        guard let image = UIImage(contentsOfFile: imagePath) else {
            return nil
        }

        UIGraphicsBeginImageContext(size)
        // decompress image by drawing it
        image.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        let resultImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return resultImage
    }

    /// 使用 Image IO 創(chuàng)建 UIImage,通過指定 option 使其在創(chuàng)建 image 就進(jìn)行解壓
    static func decompress(_ imagePath: String) -> UIImage? {
        var image: UIImage?

        let imageURL = URL(fileURLWithPath: imagePath) as CFURL
        let options = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceThumbnailMaxPixelSize: 500
        ] as CFDictionary

        if let source: CGImageSource = CGImageSourceCreateWithURL(imageURL, nil),
            let imageRef: CGImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options) {
            image = UIImage(cgImage: imageRef)
        }

        return image
    }
}

使用

DispatchQueue.global().async {
    var images: [UIImage] = []
    for i in 0 ... 221 {
        let imagePath = Bundle.main.path(forResource: "\(i)", ofType: "png")!
        if let decompressedImage = Utils.decompress(imagePath, in: CGSize(width: 400, height: 300)) {
            images.append(decompressedImage)
        }
//        if let decompressedImage = Utils.decompress(imagePath) {
//            images.append(decompressedImage)
//        }
    }

    self.decompressedImage = UIImage.animatedImage(with: images, duration: 5)!
}

// 使用
imageView.image = self.decompressedImage

  • 使用 CGContext 解碼:丟失的幀數(shù)為 0,內(nèi)存變化為 17 MB -> 25MB
  • 使用 ImageIO 解碼:丟失的幀數(shù)為 1,內(nèi)存變化為 17 MB -> 24MB

通過對比我們發(fā)現(xiàn)使用兩種方式解壓效果是基本相同的,那么兩者的優(yōu)劣勢是怎么呢

  • CGContext 解碼:
    • 優(yōu)勢:使用 Core Graphic 創(chuàng)建的 image 在繪制時(shí)有優(yōu)化,繪制更快;可以指定生成的 image 的 size,匹配 imageView 的大小可以提升一定效率;
    • 劣勢:使用 Core Graphic 會占用一定的 CPU 資源,對性能有影響
  • ImageIO 解碼:
    • 劣勢:在使用過程中我發(fā)現(xiàn)不一定能解碼,多次測試后總結(jié)出來只有 CGImageSourceCreateThumbnailAtIndex() 方法配上 options 的 kCGImageSourceCreateThumbnailWithTransform: true 才能解碼成功。使用 CGImageSourceCreateImageAtIndex() 方法這些都沒辦法立刻解碼,具體原因我也不清楚了。

5. iOS 13 新增功能

public init?(named name: String)

方法在 iOS 13 中有一句介紹是這么說的

When searching the asset catalog, this method prefers an asset containing a symbol image over an asset with the same name containing a bitmap image. Because symbol images are supported only in iOS 13 and later, you may include both types of assets in the same asset catalog. The system automatically falls back to the bitmap image on earlier versions of iOS. You cannot use this method to load system symbol images; use the init(systemName:) method instead.

簡單來說就是在 iOS 13 之后該方法查找 image 會優(yōu)先使用 symbol image ,沒有的話再使用 bitmap image。iOS 13 之前只會使用 bitmap image 。

bitmap image 就是我們之前使用的圖片,都是位圖。我們在使用過程中也知道這種圖在放大后會失真。

那什么是 symbol image 呢,簡單來說就是放大縮小不會失真的圖片,有使用過 iconfont 應(yīng)該很好理解。并且系統(tǒng)有內(nèi)置一些 symbol image 給我們使用,可以通過以下方式使用

let image = UIImage(systemName: "multiply.circle.fill")

至于 symbol image 如何制作、使用,可以參考以下文檔

Creating Custom Symbol Images for Your App
Configuring and Displaying Symbol Images in Your UI

作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍和一個(gè)交流圈子特別重要,給大家推薦一個(gè)交流群,761407670(備注123),大家有興趣可以進(jìn)群里一起交流學(xué)習(xí)!

收錄:原文地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容