简介
图片是一种应用非常广泛的文件格式,APP中也存在大量的图片图标
方案
图片基础知识
图片的描述
图片的描述主要分为位图(bitmap)和矢量图(vector),图片放大之后我们可以清晰的看到它们的本质和区别
位图bitmap
bitmap就是a map of bit,由图片上一组像素点组成,每个点又是通过RGB三原色组成。如果是透明图片的话,我们还需要信息来描述透明的程度。这是位图描述
矢量图vector
把图片看成是几何图案的组成的话,那么可以描述成图形绘制,比如绘制一条线,在画布坐标上从哪里绘制到哪里,用了什么颜色。通过把这些几何描述组成起来形成一个图片。这个是矢量描述
图片压缩
为什么要压缩图片呢?因为数据越小,网络传输越快。
有损压缩
在位图描述中,我们通过对每个点的RGB和透明度的描述来确定图片的信息,RGB每一个颜色域通常使用256色来描述。当时人眼对颜色的区分程度是不一样的,比如在某些颜色上通过128或者64色来描述,肉眼就已经无法区分,那么在这个信息里面我们就可以做压缩,这个是有损压缩。
无损压缩
无损压缩是只对冗余的数据进行处理,从而可以完全恢复数据。
图片格式
- Heic
- Webp
- jpg
- png
- Bmp
- Svg
以上是常见的图片格式,其中bmp是位图格式,svg是矢量图格式,jpg是有损压缩,png是无损压缩,heic和webp是主流的高压缩比图片格式
基准对比
阿里云OSS图片格式转换后的测试
| Data Size | Image Size | |
|---|---|---|
| 原图(png) | 97KB | 1041KB |
| heic | 10KB | 1041KB |
| webp | 32KB | 1041KB |
| Data Size | Image Size | |
|---|---|---|
| 原图(png) | 11MB | 390MB |
| webp | 1MB | 292MB |
| jpg | 7MB | 390MB |
| png | 10MB | 390MB |
| Data Size | Image Size | |
|---|---|---|
| 原图(png) | 11MB | 390MB |
| webp | 1MB | 292MB |
| jpg | 7MB | 390MB |
| png | 10MB | 390MB |
相同的图片,不同的图片格式,在data size,image size,图片清晰度等多个维度上都有区别
图片编码/解码
图片压缩是如何做到的呢?在压缩那段我们说到有损压缩是通过剔除肉眼不敏感的颜色信息来处理,那么无损压缩是如何做的呢?
霍夫曼编码
简单来说,就是把重复最多的信息使用最少的位数来描述。
例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用霍夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节,即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。
解码就是相反的过程,把使用最少位数信息描述的信息还原成之前的信息。
图片流程
iOS图片加载流程
图片源
Assets Catalog
图片位于assets catalog中有多项优化
- 优化名称和特征查询
- 更好的buffer缓存
- 针对设备的瘦身
- 矢量优化
加载方式
ImageNamed
会将图片资源缓存进入内存中
// Assets.xcassets内资源只能用过此方法调用
UIImage(named: "图片名")
Bundle
Application/Framework bundle中的图片
加载方式
imageWithContentsOfFile
// 获取目录内图片文件时
Bundle.main.path(forResource: "文件名", ofType: "png")
UIImage(contentsOfFile: "文件路径")
Documents/Caches Directories
加载方式
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let imageURL = documentsDirectory.appendingPathComponent("myImage.jpg")
do {
let imageData = try Data(contentsOf: imageURL)
guard let image = UIImage(data: imageData) else {
print("Error loading image")
return
}
// Do something with the image
} catch {
print("Error loading image: (error)")
}
}
Network
通过网络加载的图片,通常使用对象存储服务OSS,并使用CDN做加速
OSS
OSS可以设置url参数对图片进行一些处理,常见的例如图片缩放、质量变换、格式转换。图片处理操作是同步处理,处理之后会缓存起来。
以阿里云OSS为例,还可以执行图片缩放、图片水印、自定义裁剪、质量变换、格式转换、获取信息、自适应方向、内切圆、索引切割、圆角矩形、模糊效果、旋转、渐进显示、获取图片主色调、亮度、锐化、对比度等操作。
图片处理URL,自定义裁剪缩放的参数使用URL Query的方式
oss-console-img-demo-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/example.jpg…
图片解码绘制
UIkit
CGContextDrawImage
CGRect imageRect = CGRectMake(0.0f, 0.0f, CGImageGetWidth(self.image.CGImage), CGImageGetHeight(self.image.CGImage));
CGFloat imageScale = self.view.frame.size.width/imageRect.size.width;
imageRect.size = CGSizeMake(imageRect.size.width * imageScale, imageRect.size.height * imageScale);
UIGraphicsBeginImageContext(imageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, imageRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, imageRect, self.image.CGImage);
CGContextRestoreGState(context);
UIImage *handledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.imageView.image = handledImage;
Core Graphics
ImageI/O
import ImageIO
if let url = Bundle.main.url(forResource: "myImage", withExtension: "png") {
do {
let options: [CFString:Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: 1000
]
let imageData = try Data(contentsOf: url)
guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) else {
print("Error creating image source")
return
}
guard let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else {
print("Error creating image")
return
}
// Do something with the downscaled CGImage
} catch {
print("Error loading image: (error)")
}
}
Core Image
import CoreImage
if let url = Bundle.main.url(forResource: "myImage", withExtension: "png") {
do {
let imageData = try Data(contentsOf: url)
let ciImage = CIImage(data: imageData)
// Do something with the CIImage
} catch {
print("Error loading image: (error)")
}
}
vImage
import Accelerate.vImage
if let url = Bundle.main.url(forResource: "myImage", withExtension: "png") {
do {
let imageData = try Data(contentsOf: url)
let imageSource = try CGImageSource(data: imageData as CFData)
guard let image = imageSource?.cgImage(at: 0, options: nil) else {
print("Error loading image")
return
}
var format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: nil, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue), version: 0, decode: nil, renderingIntent: .defaultIntent)
var buffer = vImage_Buffer()
defer {
free(buffer.data)
}
vImageBuffer_InitWithCGImage(&buffer, &format, nil, image, numericCast(kvImageNoFlags))
// Do something with the vImage buffer
} catch {
print("Error loading image: (error)")
}
}
高清大图瓦片加载
当需要加载例如地图和单反直出的高清高分辨率的大图时,而直接使用downsampling加载整张图片会选取和减少图片像素,降低图片质量。
此时我们需要使用瓦片式的加载完整图片,瓦片式加载会根据可视区域和缩放的比例,分区域的加载不同缩放下经过downsampling的瓦片。
并且在Scrollview缩放的过程中,内存会回收释放。
CATiledLayer
#import "DuImageBrowserTiledView.h"
#import <QuartzCore/QuartzCore.h>
@interface DuImageBrowserTiledView() {
CGFloat imageScale;
CGRect imageRect;
}
@end
@implementation DuImageBrowserTiledView
+ (Class)layerClass {
return [CATiledLayer class];
}
- (id)initWithFrame: (CGRect)frame
image: (UIImage *)image
scale: (CGFloat)scale {
if ((self = [super initWithFrame:frame])) {
self.image = image;
imageRect = CGRectMake(0.0f, 0.0f, CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));
imageScale = scale;
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
tiledLayer.levelsOfDetail = 4;
tiledLayer.levelsOfDetailBias = 4;
tiledLayer.tileSize = CGSizeMake(512.0, 512.0);
}
return self;
}
- (void)drawRect: (CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextScaleCTM(context, imageScale, imageScale);
CGContextTranslateCTM(context, 0, imageRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, imageRect, self.image.CGImage);
CGContextRestoreGState(context);
}
@end
配置
levelsOfDetail
该layer维护的层级数目,每一个层级的分辨率是上一层级的二分之一
levelsOfDetailBias
layer的放大级别重绘设置,每一个层级分辨率是后面层级的两倍
tileSize
瓦片的最大尺寸
内存机制
Buffer通常用于解决不同速率间的传输,使用一片连续的内存空间,一系列内部结构相同、大小相同的元素组成的内存区域。
在Image Loading过程中有三种 Buffer
- Data Buffer
- Image Buffer
- Frame Buffer
Data Buffer
Data Buffer 是存储在内存中的原始数据,图像可以使用不同的格式保存,如 jpg、png。Data Buffer 的信息不能用来描述图像的像素信息。
Image Buffer
Image Buffer 是图像在内存中的存在方式,其中每个元素描述了一个像素点。Image Buffer 的大小和图像的大小成正比。
Frame Buffer
Frame Buffer 和 Image Buffer 内容相同,不过其存储在 vRAM(video RAM)中,而 Image Buffer 存储在 RAM 中。
而解码就是从 Data Buffer 生成 Image Buffer 的过程。Image Buffer 会上传到 GPU 成为 Frame Buffer,GPU 以每秒60次的速度使用 Frame Buffer 更新屏幕。
缓存机制
主流的三方图片加载库,普遍采用内存和磁盘两级缓存,LRU的缓存淘汰策略,以及Cache Size,Expiration Time等配置
由于网络加载耗时在图片从加载解码到渲染的整体耗时中占比过大,所以设计更好的缓存机制,提升缓存命中率这个重要指标也尤其重要
由于OSS会使用Url Query的方式修改URL,如果缓存仍以完整的URL作为存取的key的话,一方面会导致缓存命中率的降低。
另一方面,对于相同资源的图片,如果仅是分辨率不同,想要复用近似的图片缓存还需要考虑实际的图片清晰度和兼容范围。例如example.jgp?size=100,100与example.jgp?size=200,300的图片的复用策略