一张图片引发的思考

2018-10-12  本文已影响59人  darrenW

背景:

前段时间做微信小程序分享,用了某家的SDK,然鹅......他们家SDK只能上传pngjpeg格式的图片,微信不是可以上传Data吗????

???.jpeg
我吭哧吭哧半天用UIImageJPEGRepresentation压缩图片,然后在生成图片,也没把图片传上去。我当时想肯定是图片大小有问题,因为微信限制128KB以内。我查看保存在沙盒里的图片才32KB啊??怎么会上传不上去呢?再查看ImageData大小,噗~~~168KB。好吧,我被打败了。最后还是用微信原生SDK才搞定,直接传一个Data过去,多开心,多easy。

正文

好的,扯了这么多,其实就是想说一下为啥会有今天这篇大水文。在解决问题的过程中,我对iOS加载图片的理解稍微深入了那么一丢丢。现在,就水一下我理解的那么一丢丢东西。

图片经过哪些流程加载到屏幕上

  1. 从磁盘拷贝数据到内核缓冲区
  2. 从内核缓冲区复制数据到用户空间(内存级别拷贝)
  3. 生成UIImage,把UIImage赋值给UIImageView
  4. 如果图像数据为未解码的PNG/JPG,解码为位图数据
  5. 隐式CATransaction捕获到UIImageView图层树的变化
  6. 主线程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了。(解压缩后的数据)

恍然大悟.jpg

压缩图片

不过分享某一张图片的时候,我用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
}

图片加载

通常我们说图片加载会用到两种方法:imageNamedimageWithContentsOfFile,我们简单介绍这两种方法

imageNamed

该方法的特点在于可以缓存已经加载的图片;使用时,先根据文件名在系统缓存中寻找图片,如果找到了就返回;如果没有,就在Bundle内查找到文件名,找到后把这个文件名放到UIImage里返回,并没有进行实际的文件读取和解码。当UIImage第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。

imageWithContentsOfFile

该方法仅加载图片,不缓存图像数据,其解码依然要等到第一次显示该图片的时候。

对于这两种方法,我们可以做出如下比较:

此外,在 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中的图片使用方式、内存对比和最佳实践

上一篇 下一篇

猜你喜欢

热点阅读