iOS - 图片使用优化的一些总结

最近在使用动图时

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

发现一个现象,因为 animatedImage 需要加载大量 image,所以在加载时会造成一定的卡顿。因此我想通过一些测试记录一下 image 在使用过程中的一些优化

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 关闭 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 结论

  1. 图片大小对包大小是有一定影响的,最好对工程中所有图片进行压缩
  2. png 图片放在 Assets 中,iOS 会对其进行进一步的压缩,可以考虑使用 Assets 管理 png 图片
  3. Building Setting 中的 Compress PNG Files - Packaging 选项关闭可以稍微减少包大小,但是会影响 png 在使用时加载的速度,所以建议保持默认值,开启

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.

此方法会从系统缓存检查是否具有指定名称的 UIImage 对象。如果缓存中不存在匹配的图像对象,则此方法将从 assets 目录创建图像或从磁盘加载图像。系统可以随时清除缓存的图像数据以释放内存。仅对缓存中但当前未使用的图像进行清除。

  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.

此方法将图像数据加载到内存中并将其标记为可清除。如果清除数据并需要重新加载,则图像对象将从指定路径再次加载该数据。

2.1 结论

对于方法两个方法的主要区别就是

  • 方法一:创建的 UIImage 的加载到内存中后,会一直存在内存中,及时持有 UIImage 的对象(如 UIImageView)释放了也不会释放。以及加载到内存中时就是解码后的图片。
  • 方法二:没有对象(如 UIImageView)持有该 UIImage,会从内存中释放,下次使用时会从指定的 path 重新加载。在内存中不是解码后的图片,需要在渲染前额外进行解码操作。

这就造成了两者的使用场景不同

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

2.2 理解误区

同时这里有一个我之前一直理解错误的误区,因为之前看过各种各样的资料,说的不尽相同。也是这次自己编写代码测试过后才重新确定了一些逻辑。

当我们执行以下代码

let image = UIImage(named: "1")

或者

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

此时只是创建了 UIImage 对象,并不会把图片加载到内存(测试过程中未看到使用内存增加)中以及进行图片解码(未造成卡顿)

只有使用到 UIImage 时,即将 image 赋值给 layer 的 contents 或者 imageView 的时候,才会加载 image 到内存以及解码,这个时候才是一些卡顿发生的时间。

我看 iOS Core Animation: Advanced Techniques 在讲如何避免延迟解码带来的卡顿有这样一段话,给我带来很多的干扰

使用 UIImage+imageNamed: 方法避免延时加载。不像 +imageWithContentsOfFile:(和其他别的UIImage加载方法),这个方法会在加载图片之后立刻进行解码。

我品了品这句话的意思,说的有道理,其实并没什么卵用。因为加载的时机都是将 image 赋值给 layer 的 contents,此时用 +imageNamed: 会解码完存在内存中;+imageWithContentsOfFile 直接存在内存中,然后在渲染前解码。两者比较其实 +imageNamed: 并没有把解码时机提前,卡顿时间应该是一样的,它的优势仅仅在频繁使用时只要解码一次

3. png 、jpg 加载速度测试

测试环境

  • iPhone7 真机

  • iOS 13.1

测试不同大小的 png、jpg 图片加载以及解码的耗时,图片越大加载速度越慢,png 往往比 jpg 大,但是 png 解码速度相比 jpg 又有优势,总耗时要综合考虑两个因素。图片都是有 Mac 上自带的 预览 导出的,计算一百次取平均值

时间计算代码

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)
}

结果

类型 大小 时间 / 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 结论

  • 对于大图,可以使用 jpg,不仅体积小,加载速度快
  • 对于小图,可以使用 png,此时 png 和 jpg 大小差异不明显,选择解码速度快的

4. animatedImage 加载优化

实现显示一个 animatedImage 会卡顿的优化,测试用的动图总共 222 张,大小为 500 x 375,总共的大小为 2.8 MB

计算卡顿的方式使用 CADisplayLink,当发生卡顿时,输出总共丢失的帧数

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

/// 当发生卡顿时,输出丢失的帧数
@objc private func tick(_ link: CADisplayLink) {
    if lastTime == 0 {           // 对lastTime进行初始化
      lastTime = link.timestamp
      return
    }

    let delta = link.timestamp - self.lastTime;  //计算本次刷新和上次更新FPS的时间间隔

    let timeInterval = 1.0 / 60.0

    if delta > timeInterval * 1.5 {
      let lostFrame = delta / timeInterval
      print(String(format: "卡顿的帧数:%.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

输出的结果如下,前面的 16 帧丢失应该是加载图片造成的,后面的 25 帧丢失应该是图片解码造成的

卡顿的帧数:16.00
卡顿的帧数:25.00

同时查看使用的内存为 17 MB -> 180 MB

4.2 提前一步加载解码图片

我们在使用动图之前提前对图片进行加载和解码

我们解码的方法如下

struct Utils {
    /// 使用 CGContext,绘制时会自动进行解码
    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 创建 UIImage,通过指定 option 使其在创建 image 就进行解压
    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 解码:丢失的帧数为 0,内存变化为 17 MB -> 25MB
  • 使用 ImageIO 解码:丢失的帧数为 1,内存变化为 17 MB -> 24MB

通过对比我们发现使用两种方式解压效果是基本相同的,那么两者的优劣势是怎么呢

  • CGContext 解码:
    • 优势:使用 Core Graphic 创建的 image 在绘制时有优化,绘制更快;可以指定生成的 image 的 size,匹配 imageView 的大小可以提升一定效率;
    • 劣势:使用 Core Graphic 会占用一定的 CPU 资源,对性能有影响
  • ImageIO 解码:
    • 劣势:在使用过程中我发现不一定能解码,多次测试后总结出来只有 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 会优先使用 symbol image ,没有的话再使用 bitmap image。iOS 13 之前只会使用 bitmap image

bitmap image 就是我们之前使用的图片,都是位图。我们在使用过程中也知道这种图在放大后会失真。

那什么是 symbol image 呢,简单来说就是放大缩小不会失真的图片,有使用过 iconfont 应该很好理解。并且系统有内置一些 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

作为一个开发者,有一个学习的氛围和一个交流圈子特别重要,给大家推荐一个交流群,761407670(备注123),大家有兴趣可以进群里一起交流学习!

收录:原文地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容