背景:
前段时间做微信小程序分享,用了某家的SDK,然鹅......他们家SDK只能上传png
、jpeg
格式的图片,微信不是可以上传Data
吗????
我吭哧吭哧半天用
UIImageJPEGRepresentation
压缩图片,然后在生成图片,也没把图片传上去。我当时想肯定是图片大小有问题,因为微信限制128KB以内。我查看保存在沙盒里的图片才32KB啊??怎么会上传不上去呢?再查看Image
的Data
大小,噗~~~168KB。好吧,我被打败了。最后还是用微信原生SDK才搞定,直接传一个Data
过去,多开心,多easy。
正文
好的,扯了这么多,其实就是想说一下为啥会有今天这篇大水文。在解决问题的过程中,我对iOS加载图片的理解稍微深入了那么一丢丢。现在,就水一下我理解的那么一丢丢东西。
图片经过哪些流程加载到屏幕上
- 从磁盘拷贝数据到内核缓冲区
- 从内核缓冲区复制数据到用户空间(内存级别拷贝)
- 生成
UIImage
,把UIImage
赋值给UIImageView
- 如果图像数据为未解码的PNG/JPG,解码为位图数据
- 隐式
CATransaction
捕获到UIImageView
图层树的变化 - 主线程
Runloop
提交CATransaction
,开始进行图像渲染
6.1 如果数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐
6.2 GPU处理位图数据,进行渲染
其中第四点就是导致我32KB变168KB的“罪魁祸首”。为啥这么说呢?先了解一些东西。
PNG
PNG
只支持无损压缩,所以它的压缩比是有上限的。它有alpha
通道,支持图片透明。此外xcode会对png格式进行特殊的优化处理,而对于其他图片不做处理,所以我们一些小图标经常用PNG
。
JPEG
JPEG
支持有损压缩,不含有alpha
通道,它可以通过图片质量换取内存空间。网络图片最好选用JPEG
,可以节省流量、提高下载速度。
位图
我们是否可以直接使用图片,使其显示在屏幕上呢?答案显然后不可以。图片经过解压后,变成位图数据。那么位图是什么呢?苹果给出的解释是
A bitmap image (or sampled image) is an array of pixels (or samples)
位图是一个像素数组。至于怎么将像素绘制到屏幕上,可以看这篇文章,就不做过多叙述(人家说的很明白)。
解码
解码其实就是将图片的二进制数据转换成像素数据。这个过程是比较耗时的,不能使用 GPU 硬解码,只能通过 CPU 软解码实现(硬解码是通过解码电路实现,软解码是通过解码算法、CPU 的通用计算等方式实现软件层面的解码,效率不如 GPU 硬解码)。解码后的文件大小计算公式
解压缩后的图片大小 = 图片的像素宽 * 图片的像素高 * 每个像素所占的字节数 (4)
每个像素所占的字节数为什么是4呢?因为我们所使用的位图大部分是32位的RGBA模式,这种模式位图的一个像素所占内存为32位,也就是4个字节的长度 。出处在此
所以,本地保存的32KB的图片,解码就是168KB了。(解压缩后的数据)
压缩图片
不过分享某一张图片的时候,我用UIImageJPEGRepresentation
方法压缩不到128KB一下???什么图片这么大?后来问一下后台才知道,这张图片是相机拍摄的,尺寸非常大,只能重新设置图片尺寸。献上我的代码
func compressImage(_ image: UIImage, toByte maxLength: Int) -> Data?{
var compression: CGFloat = 1
var data = UIImageJPEGRepresentation(image, compression)!
if data.count <= maxLength {
return data
}
var max: CGFloat = 1
var min: CGFloat = 0
let newSize = CGSize.init(width: 200, height: 160)
UIGraphicsBeginImageContext(newSize)
image.draw(in: CGRect.init(x: 0, y: 0, width: newSize.width, height: newSize.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
data = UIImageJPEGRepresentation(newImage, 1.0)!
if data.count <= maxLength {
return data
}
for _ in 0..<10 {
compression = (max + min) / 2
data = UIImageJPEGRepresentation(newImage, compression)!
if CGFloat(data.count) < CGFloat(maxLength) * 0.9 {
min = compression
} else if data.count > maxLength {
max = compression
} else {
break
}
}
return data
}
图片加载
通常我们说图片加载会用到两种方法:imageNamed
、imageWithContentsOfFile
,我们简单介绍这两种方法
imageNamed
该方法的特点在于可以缓存已经加载的图片;使用时,先根据文件名在系统缓存中寻找图片,如果找到了就返回;如果没有,就在Bundle
内查找到文件名,找到后把这个文件名放到UIImage
里返回,并没有进行实际的文件读取和解码。当UIImage
第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。
imageWithContentsOfFile
该方法仅加载图片,不缓存图像数据,其解码依然要等到第一次显示该图片的时候。
对于这两种方法,我们可以做出如下比较:
- 本地(Assets)保存的图标加载使用
imageNamed
- 经常使用且文件不大的图片使用
imageNamed
- 对于一些文件较大的图片使用
imageWithContentsOfFile
,当然最好的办法是用UIGraphicsBeginImageContext
方法重新绘制图片
此外,在 WWDC 2018上,苹果为我们建议了一种大家平时使用较少的大图加载方式,它的实际占用内存与理论值最为接近。
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)
}
参考
iOS图片加载速度极限优化—FastImageCache解析
谈谈 iOS 中图片的解压缩
iOS中的图片使用方式、内存对比和最佳实践