iOS开发图片格式选择

图片是如何显示的

在讲解如何选择图片格式之前,我感觉有必要先了解下,图片是如何展示的。如果我们要展示一张图片,一般步骤是这样的:

/// Assets.xcassets中的图片,不需要后缀
let image = UIImage(named: "icon")
let imageView = UIImageView(frame: rect)
imageView.image = image
view.addSubview(imageView)

运行程序,我们就可以在指定位置看到这个icon。看似简单的代码背后隐藏了很多细节工作。一张图片的展示,从代码执行到展示出来大致经历了这些步骤:

1. 加载图片

  • 从磁盘中加载一张图片;

  • 然后将生成的 UIImage 赋值给 UIImageView

  • 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;

  • 分配内存缓冲区用于管理文件 IO 和解压缩操作,将文件数据从磁盘读到内存中;

2. 图片解码(解压)

  • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,默认在主线程进行;

3. 图片渲染

  • Core AnimationCALayer使用解压(解码)的位图数据渲染 UIImageView 的图层;

  • CPU计算好图片的Frame,对图片解压之后,就会交给GPU来做图片渲染渲染流程;

  • GPU获取获取图片的坐标,将坐标交给顶点着色器(顶点计算),将图片光栅化(获取图片对应屏幕上的像素点),片元着色器计算(计算每个像素点的最终显示的颜色值);

  • 从帧缓存区中渲染到屏幕上;

这其中有个关键步骤是图片解码。那为什么要解码呢,这是因为我们平常使用的图片一般为了节约空间都会经过一些压缩算法进行封装,而使用时屏幕要精确的渲染到每个像素点,这就需要把压缩的图片解码展开,便于系统处理。

名词解释

有损vs无损

有损压缩:指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量,并且这种损失是不可逆的,我们不可能从有一个有损压缩过的图片中恢复出全来的图片。常见的有损压缩手段,是按照一定的算法将临近的像素点进行合并。

无损压缩:只在压缩文件大小的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。

索引色vs直接色

索引色:用一个数字来代表(索引)一种颜色,在存储图片的时候,存储一个数字的组合,同时存储数字到图片颜色的映射。这种方式只能存储有限种颜色,通常是256种颜色,对应到计算机系统中,使用一个字节的数字来索引一种颜色。

直接色:使用四个数字来代表一种颜色,这四个数字分别代表这个颜色中红色、绿色、蓝色以及透明度。现在流行的显示设备可以在这四个维度分别支持256种变化,所以直接色可以表示2的32次方种颜色。当然并非所有的直接色都支持这么多种,为压缩空间使用,有可能只有表达红、绿、蓝的三个数字,每个数字也可能不支持256种变化之多。

点阵图vs矢量图

点阵图:也叫做位图,像素图。构成点阵图的最小单位是象素,位图就是由象素阵列的排列来实现其显示效果的,每个象素有自己的颜色信息,在对位图图像进行编辑操作的时候,可操作的对象是每个象素,我们可以改变图像的色相、饱和度、明度,从而改变图像的显示效果。点阵图缩放会失真,用最近非常流行的沙画来比喻最恰当不过,当你从远处看的时候,画面细腻多彩,但是当你靠的非常近的时候,你就能看到组成画面的每粒沙子以及每个沙粒的颜色。

矢量图:也叫做向量图。矢量图并不纪录画面上每一点的信息,而是纪录了元素形状及颜色的算法,当你打开一张矢量图的时候,软件对图形象对应的函数进行运算,将运算结果[图形的形状和颜色]显示给你看。无论显示画面是大还是小,画面上的对象对应的算法是不变的,所以,即使对画面进行倍数相当大的缩放,其显示效果仍然相同(不失真)。

几种格式的对比

一张图片,如果我们将它的每一个像素及其对应的颜色都存储起来(BMP格式),是会很大的。为了减小图片占用的存储空间,派生出了各种不同压缩算法所代表的图片格式。常见的图片格式有png、jpeg、heic、gif、webp,svg等。

PNG

PNG有两种类型:PNG-8和PNG-24。

  • PNG-8是无损的、索引色、点阵图。它支持透明度的调节。

  • PNG-24是无损的、直接色、点阵图。因为使用直接色,颜色范围更大,也占用更大的空间。他的目标是替代JPEG,但一般而言,PNG-24的文件大小是JPEG的五倍之多,而显示效果则通常只能获得一点点提升。所以如果不是对图片质量要求特别高,不建议使用PNG-24

PNG是苹果推荐的图片格式,而且这些图片会被一个叫pngcrush的开源工具优化,这样iOS设备就能在显示时更快的解压和渲染图片。该工具位于目录:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin

JPEG

JPEG是有损的、采用直接色的、点阵图压缩方式。 JEPG目标是在不影响人类可分辨的图片质量的前提下,尽可能的压缩文件大小。一般都是用于相机图片的格式。但因为有损,会导致图片失真。iOS可以通过以下方式压缩图片:

// 压缩比范围从0到1
func jpegData(compressionQuality: CGFloat) -> Data?

题外话 一般来说,相同的图片采用JPEG的压缩方式会比png得到更小的尺寸,但也有例外。

在网上查了资料说是JEPG更适合处理带有很多杂色的风景图,而对于使用数位板等电子绘制的纯色卡通系风格图片,JEPG的压缩方式会适得其反,导致体积更大。

HEIC

HEIC是HEIF(High Efficiency Image Format 高效率图像文件格式)的一种。它并非苹果开发,而是由运动图像专家组(MPEG)开发。它同时支持有损压缩、无损压缩、透明度等特性。HEIF规范的完成是在2015年,是这几种图片格式中最新的一种了,目前除了苹果,还没有哪家大厂去拥抱这种格式。

在iOS 11更新后,iPhone 7及其后硬件,在拍摄照片时的默认图像存储格式。与JPG相比,它占用的空间更小,画质更加无损。HEIC的目的就是作为JPEG的继任者,以后或许会成为一种趋势。目前可以想到的在开发中的应用是,对于一些需要下载的大图可以转成HEIC格式,供客户端使用。但是当前却很少应用,大概率是考虑到图片兼容问题吧。

题外话

一个有趣的现象,我用相机(iPhoneXR)拍摄一张照片,通过AirDrop传到电脑,显示为HEIC格式。当我在拍照时选择系统自带的任意一种滤镜,图片格式就变成了JPEG。这是为什么?

有小伙伴解答:

iOS拍照选择滤镜会“转”为JPEG,是因为拍照的格式还是HEIF,加滤镜和编辑图片都是相当于复制了一份再做操作的,点击复原又会“转”为HEIF。

经过测试确实是这样的,而且既然苹果提供复原的操作,说明原图(HEIF)并没有被覆盖。那为什么滤镜不能直接在HEIF格式下操作,猜测可能是跟滤镜的算法相关,该算法只能对JEPG格式编码的图片进行渲染,所以需要中间转成JEPG。

Live Photo

Live图片的实质是:一张heic格式封面图 + mov格式视频。

对于Live Photo的展示,在原生应用中可以使用PHLivePhotoView,在Web应用中可以使用LivePhotosKit JS

WebP

WebP最初由Google发布于2010年,图片格式派生自VP8视频编码,也同时支持有损压缩、无损压缩、透明度等特性。2013年低,推出了Animated WebP,还可以支持动图。

WebP 集合了多种图片文件格式的特点。它像 JPEG 一样适合压缩照片和其他细节丰富的图片,像 GIF 一样可以显示动态图片,像 PNG 一样支持透明图像。根据 Google 的测试,WebP 无损压缩图片比 PNG 图片少了 45% 的文件体积,即使这些 PNG 图片在使用 pngcrushPNGOUT 处理后,WebP 依旧可以减少 28% 的文件体积。可以在点击这里查看WebP对其它格式转换的效果。

小是WebP的最大优点,小意味着更少的流量,这也是各大流量入口在意的地方。目前Google、Facebook、阿里等大厂已经在广泛使用WebP格式,国内的一些图床服务(七牛、又拍云)也支持将图片自动转成WebP格式。

诚然WebP非常优秀,独自完成了图片格式”大一统”的任务。但苹果对WebP的支持却很少,只Safari目前还不支持WebP显示就阻断了很多人应用WebP的决心。

如果我们需要在项目中显示WebP格式图片就不得不导入Google的libwebp解码库。当然WebP的解码任务在iOS端有些库已经封装好了,OC端可以用SDWebImageWebPCoder,Swift端可以用KingfisherWebP。以下是使用Kingfisher展示WebP图像的事例:

// 全局配置对WebP图片的解码(仅针对WebP格式)
KingfisherManager.shared.defaultOptions += [
    .processor(WebPProcessor.default),
    .cacheSerializer(WebPSerializer.default)
]
// 本地webp图片解码
let localUrl = Bundle.main.url(forResource: "sunset", withExtension: "webp")
let dataProvider = LocalFileImageDataProvider.init(fileURL: localUrl!)
imageView.kf.setImage(with: dataProvider)

// 远程webp图片解码。一些图像服务器可能期望“Accept”标头包含“image/webp”,我们还需要加上
let modifier = AnyModifier { request in
    var req = request
    req.addValue("image/webp */*", forHTTPHeaderField: "Accept")
    return req
}
KingfisherManager.shared.defaultOptions += [
    .requestModifier(modifier),
    // ... other options
]

PDF

pdf图片通常是矢量的,它的导入方式有些特殊。我们需要在Assets.xcassets文件,创建一个New Image Set,然后将该文件的Scales设置为Single Scale,拖入1x尺寸的pdf文件即可:

使用时我们可以把它当做普通图片对待:

let image = UIImage(named: "sunset")

在运行期间Xcode会根据屏幕的比例因子生成对应尺寸的png图像。比如导入一张100x100的pdf图片,在2x和3x的机型里面会生成对应的200x200,300x300的png(可以在Assets.car中找到)。所以pdf只不过是Xcode处理图片的中间状态,下载到手机的应用包里面是没有这张pdf的。

这种处理方式有一个好处就是,当苹果以后发布一款4x屏幕的手机时,使用pdf处理的图片会自适应生成对应的4x资源,不需要再手动导入。但相比优点,pdf作为图片资源的缺点更多。

首先是尺寸上,因为是自动生成对应的png,并没有任何优化和压缩,而且我们也并不能在这中间做什么。对比相同尺寸经过ImageOptim压缩过的png,在大小上后者会是前者的1/2,甚至1/4。

另外pdf对阴影和渐变的处理会存在失真的情况:

左边是png,右边是pdf。在一些渐变和光影的图像部分可以看出明显的失真。

更多关于pdf和png的差别,可以看这篇:Why I don’t use PDFs for iOS assets: https://bjango.com/articles/idontusepdfs/

SVG/SF Symbol

SVG是一种无损的矢量图,是众多矢量图中的一种,它的特点是使用XML来描述图片。使用XML的优点是,任何时候你都可以把它当做一个文本文件来对待,也就是说,你可以非常方便的修改SVG图片,你所需要的只需要一个文本编辑器。

在iOS13之前应用中直接使用SVG的场景非常少,但从iOS13开始,苹果推出了SF Symbol,一种svg格式的矢量符号集。而且苹果还提供了多于1500多种icon模板,我们可以在这里下载查看。

我们可以从中选择适合自己的icon,选中之后,从File > Export Custom Symbol Templete中导出svg格式图片集,然后拖到Xcode的Assets.xcassets

SF Symbol有9种粗细的调节——从ultralight到black——每一种都相当于San Francisco系统字体的重量(weight)。(SF Symbol中的SF是San Francisco(旧金山)的缩写)。这种对应使您能够在符号和相邻文本之间实现精确的权重匹配,同时支持不同大小和上下文的灵活性。

当然如果这些图标都不能满足需求,我们还可以自定义SF图标,然后通过SF Symbol App进行验证和导出。操作细节可以看这里:Creating Custom Symbol Images for Your App

SF Symbol使用起来也很简单:

let configuration = UIImage.SymbolConfiguration.init(scale: .small)
imageView.image = UIImage(systemName: "alarm", withConfiguration: configuration)

SF Symbol可以一次性解决相同icon,不同尺寸,不同粗细的问题,它让我们处理图片像处理字体一样方便。可以想象这就是应用图标的未来。

当看到SF Symbol仅支持iOS13+,watchOS6+,我又不得不退回到现实,png也挺好的。

题外话

我在测试SF Symbol图标时,从生成的应用包中查看图片,会得到这样的结果:

代码中的我将图片设置为100x100,仅有这一处地方使用。跟pdf类似我们找不到svg源文件,这好理解,svg只是中间状态,我们最终使用的还是png,但为什么会有多个小尺寸的png图像呢?

如何选择图片格式

我们平常开发时,使用最多的就是png了,甚至可能是不加考虑的全部使用png。其实这样是不好的,我们应该充分发挥不同格式图片的优点,从兼容性空间占用展示效果三方面考量选取最佳格式。

关于图片格式的选择,苹果的Human Interface Guidelines有以下说法:

一般情况下,使用PNG图片,因为PNG支持透明性,而且是无损的,压缩工件不会模糊重要的细节或改变颜色。对于需要阴影、纹理和高光效果的复杂艺术品来说,这是一个不错的选择。使用8位的PNG图形,不需要完全24位的颜色。8位PNG可以在不降低图像质量的情况下减小文件大小。精致的应用图标最好使用png。

对于照片应该使用JPEG格式,它的压缩算法通常比无损格式产生更小的尺寸,而且很难在照片中辨别出来。应该尝试优化JPEG文件,在大小和质量之间找到平衡。大多数JPEG文件可以被压缩而不会导致图像明显的退化。即使是少量的压缩也可以节省大量的磁盘空间。

使用PDF处理字形和其他需要高分辨率缩放的平面矢量图形。

最终可以做以下总结。

图片格式适用范围注意事项
png应用icon,界面icon,卡通风格的背景图导入项目前可以使用ImageOptim进行压缩
jpeg尺寸较大的风景图,照片不支持透明度;因为可以调节压缩比,可以在大小和质量之间寻找最佳平衡。
webp支持有损、无损压缩、透明度、动图等特性,因为苹果本身不支持一般只应用于服务端返回来的图片无法在xcode预览,不建议内置该类型图片
pdf字形,高分辨率的矢量图存在展开尺寸较大,光效失真的情况
svg(sf symbol)指示性icon仅支持iOS13及以上,系统sf符号是有版权的,使用时要注意应用范围和苹果要求

引用

谈谈 iOS 中图片的解压缩

图片格式那么多,哪种更适合你

iOS图片格式选择

Why I don’t use PDFs for iOS assets

SF Symbols: The benefits and how to use them guide

WebP 的前世今生