Fork me on GitHub

iOS中的图片使用方式、内存对比和最佳实践(转)

原文链接

预备知识

  我们的对比主要关注内存的占用情况。对比的格式是 jpg 和 png 这两种最广泛使用的格式,分别代表了有损压缩和无损压缩;关于它们的特点和介绍,可以参考郭耀源的这篇文章:移动端图片格式调研。我们可以看到,在iOS 设备上它们的解码消耗在一个量级,速度较快。
  WWDC2018 苹果重点关注了图片的内存占用情况(因为此前大家的通用做法实际上是相对低效的),并且给了大家一些指导。这里我们会用 demo 实验的方式(并且通过工具来进行 benchmark),为大家揭示一些事实和现象,供大家去理解和分析,从而去形成我们代码的最佳实践。

Demo: DownSampleDemo

场景

  首先,苹果告诉我们,图片在应用中主要的内存占用(这通常发生在图片要被载入并显示时)和图片本身的大小实际是无关的;重要的是图片的尺寸。decode buffer 的计算方式是 width*height*N,这里的 N 通常是 4 (最常见的 ARGB888 格式),N取决于你显示所使用的格式。但很多时候,我们会直接把一个UIImage传递给UIImageView, 实际上该 View 的尺寸可能远远的小于 UIImage 本身。

使用图片的三种方式:

方式A

1
image1.image = UIImage(named: "000.jpg")

  这是我们最通常使用图片的方式,可能有人会想到imagewithcontentsoffile,实际上它和上面的方法只有一个不同之处: 正常情况下 imageNamed 会缓存这个图片在整个App存续期间,这样就无需重复的 decode;而 imagewithcontentsoffile 则没有这个缓存,图片不使用了就会释放内存。但这对我们的 case 并无帮助,因为我们是在使用这些图片,并且观察它们的内存占用.

方式B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (UIImage *)OriginImage:(UIImage *)image scaleToSize:(CGSize)size {

// 创建一个bitmap的context
// 并把它设置成为当前正在使用的context
UIGraphicsBeginImageContext(size);

// 绘制改变大小的图片
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];

// 从当前context中创建一个改变大小后的图片
UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();

// 使当前的context出堆栈
UIGraphicsEndImageContext();

// 返回新的改变大小后的图片
return scaledImage;
}

  这是一种被广泛使用的缩放图片的办法。

方式C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {

let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary
// 其他场景可以用createwithdata (data并未decode,所占内存没那么大),
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)
}

  这是苹果介绍给我们的新方法。

关于测试

  测试设备 : 1.iPhone8 11.4, 2.iPhone6 12beta2

  测试图片格式 : png/jpg

  测试的图片使用方式 :3种

  我们将2560*1440大小的图片放到 282*138 的 View 中,对于缩放我们使用2x的格式来显示以保证其效果。

  实验数据的细节在 DownSampleDemo 的 ViewController.swift 的注释中,这里直接告诉大家结论:

  1. Xcode 面板上所显示的内存占用并不可靠,在很多 case 下应用占用的内存要比其上显示的多的多(特别是对于iOS11.4的设备)。但可以用来粗略的查看内存占用,如果这里超了很多,那肯定是超了
  2. 可以使用 instruments 的 allocation 和 memory debugging 的 memgraph+ 命令行分析这两种方式来观测内存的占用
  3. 第一种方式下 jpg 载入后的内存分为 imageIO 和 IOkit 两部分,png 载入只有 imageIO 部分,jpg 内存占用比png要少不少
  4. 第二种方式的数据在 Xcode 面板上失真,通过另外两种方式可以看到它非常不靠谱。内存消耗甚至可能超过第一种。
  5. 第三种方式的内存占用非常完美,严格的遵守了公式计算出来的大小(跟 downsample 后的尺寸有关),内存占用在 CG raster data 中。
  6. memgraph 命令: vmmap –summary xxx.memgraph
  7. allocation 我们只需要看 VM allocation 栏目下的 dirty size 和 swapped size,这里需要手动的打开snapshot 才能看到该栏目的数据,并且需要适当的重复多次运行才能保证结果的正确性

VMTracker 运行截图:

memgraph 命令结果截图:

最后,来自苹果的最佳实践: 强烈推荐大家使用 downsample 来处理大图!

------------- 本文结束感谢您的阅读 -------------