摘要: 加载网络图片时, 时常会出现内存占用很大的问题, 这篇文章着重探究这个现象产生的原因, 以及如何解决内存占用过大的问题.
在 iOS 中图片被加载到 UImageView 上时, 其内存占用可以使用如下的方式计算:
比如分辨率为: 1920 * 1080, 则: result = 1920 * 1080 * 4 = 7.91 MB 其中 4 代表的是单位像素占用的字节数
而实际上一张 1920 * 1080 的 jpeg 文件的大小可能只有几十 KB.
所以如何在加载图片过程中对图片进行处理就成为了一个很常见的需求.
测试加载一张图片的内存占用
分别来看从网络直接加载 JPG, 从本地加载同一张图片的 PNG 格式和 JPG 格式时候的内存占用. 使用的样例图片为 1280 * 720 分辨率的.
网络加载 JPG 图片
下面先来看一个实际的加载图片例子, 代码如下所示:
let url = URL(string: "http://pic.pptbz.com/201506/2015070581208537.JPG")! let session = URLSession.shared let task = session.dataTask(with: url) {(data, resp, error) in let data = data! let image = UIImage(data: data)! print(data) print(image) } task.resume()
上面代码中打印的结果如下:
23356 bytes <UIImage: 0x101db0780>, {1280, 720}
在 Xcode 中查看到 APP 内存占用总大小为 7 MB(作为图片还未加载到 ImageView 上的一个内存占用基准参考值).
下面将从网络上获取的图片通过 ImageView 展示出来:
let imageView = UIImageView() view.addSubview(imageView) imageView.frame = CGRect(x: 100, y: 100, width: 200, height: 200) // ... let url = URL(string: "http://pic.pptbz.com/201506/2015070581208537.JPG")! let session = URLSession.shared let task = session.dataTask(with: url) {(data, resp, error) in let data = data! let image = UIImage(data: data)! DispatchQueue.main.async { imageView.image = image } } task.resume()
加载后发现内存占用仅仅提高了约 300 KB. 看来从网络加载的图片并没有显著增加内存占用.
从本地加载同一图片的 PNG 和 JPG 格式
将之前使用的网络图片下载到本地, 在不加载图片的情况下, 多次执行 APP, 发现内存占用为 6.5 MB 到 6.9 MB 之间, 取 6.7 MB 作为参考的基准值.
(和上述网络上加载的图片是同一张, 即分辨率是相同的, 只是文件格式不同而已).
-
该图片保存到本地, 格式为 PNG, 此时图片的文件大小是 123 KB.
直接用
UIImage(named:)方式拿到图片, 并显示到 UIImageView 中, 发现内存占用图片飙升, 达到约 10 MB. 即增加了近 3.3 MB 的内存占用, 使用之前介绍的计算方式可以计算一张 1280 * 720 的图片内存占用约 3.5 MB, 符合预期结果. -
图片格式为 JPG 时, 采用相同的条件加载:
swiftlet bundle = Bundle.main let jpg = bundle.url(forResource: "1", withExtension: "JPG") let data = try! Data(contentsOf: jpg!) let image = UIImage(data: data) print(data) print(image)打印结果如下:
bash23356 bytes Optional(<UIImage: 0x10165c450>, {1280, 720})可以看到实际上和网络加载时候的打印结果相同.
将该 image 显示到 UIImageView 上, 此时内存占用经多次测试后发现内存占用仅提升 200 KB.
经上述过程, 可以得出结论1:
在加载图片时, 使用 JPG 格式可以比 PNG 占用少得多的内存. 和图片是在网络还是在本地无关.
下面剩下的问题就是如何有效降低 PNG 图片的内存占用.
降低 PNG 图片的内存占用
使用如下代码加载网络上的一张 png 图片:
let url = URL(string: "http://pic31.nipic.com/20130702/13133954_121018329102_2.png")! let session = URLSession.shared let task = session.dataTask(with: url) { (data, resp, error) in let data = data! let image = UIImage(data: data)! print(data) print(image) DispatchQueue.main.async { self.imageView.image = image } } task.resume()
打印结果为:
78144 bytes <UIImage: 0x1017eaa70>, {1024, 576}
内存占用提升约 2.3 MB, 符合预期.
下面就来介绍收集到的一些比较有效的减少 PNG 图片加载时内存占用的办法.
调整尺寸后重绘
这种方法的特点就是简单, 但属于 CPU 消耗型的操作, 在多张图片都需要这样的操作时就显得很不适用了.
如下是代码:
private func resizeAndRedrawImageWith(image: UIImage, newSize: CGSize) -> UIImage { UIGraphicsBeginImageContextWithOptions(newSize, false, 0) let rect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height) image.draw(in: rect) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage! }
使用时如下所示:
let newImage = self.resizeAndRedrawImageWith( image: image, newSize: self.imageView.bounds.size ) self.imageView.image = newImage
内存占用从之前的 9.3 兆降低到 7兆左右, 效果还是非常明显的. 但缺点也非常明显, CPU 消耗太大.
向下采样
这种是在网上找到的, 据传是苹果推荐的方法.
如下是具体实现代码:
// 参数为图片 URL private func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage { let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary let source = CGImageSourceCreateWithURL(imageURL as CFURL, sourceOpt)! let maxDimension = max(pointSize.width, pointSize.height) * scale let downsampleOpt = [kCGImageSourceCreateThumbnailFromImageAlways : true, kCGImageSourceShouldCacheImmediately : true , kCGImageSourceCreateThumbnailWithTransform : true, kCGImageSourceThumbnailMaxPixelSize : maxDimension] as CFDictionary let downsampleImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOpt)! return UIImage(cgImage: downsampleImage) } // 参数为图片 Data 数据 private func downsample(data imageData: Data, to pointSize: CGSize, scale: CGFloat) -> UIImage { let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary let source = CGImageSourceCreateWithData(imageData as CFData, sourceOpt)! let maxDimension = max(pointSize.width, pointSize.height) * scale let downsampleOpt = [kCGImageSourceCreateThumbnailFromImageAlways : true, kCGImageSourceShouldCacheImmediately : true , kCGImageSourceCreateThumbnailWithTransform : true, kCGImageSourceThumbnailMaxPixelSize : maxDimension] as CFDictionary let downsampleImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOpt)! return UIImage(cgImage: downsampleImage) }
使用上同样很简单:
let newImage = self.downsample(data: data, to: self.imageView.bounds.size, scale: 1.0) self.imageView.image = newImage
内存占用也降了约 2 兆多, 效果明显. 且这个方法的 CPU 消耗相对第一个方法而言小很多, 十分推荐采用!