iOS中图片的解压缩

文章很大一部分参考了这篇博文雷纯锋-谈谈 iOS 中图片的解压缩,感兴趣的可以直接看

我们一般在界面上显示一张图片都先生成UIImage然后将其赋给UIImageView,再设置ImageView的坐标和大小就可以,如下:

UIImage *originalImage = [UIImage imageNamed:@"logo"];
UIImageView *orignalImageView = [[UIImageView alloc]initWithImage:originalImage];

然而,实际上系统帮我们做了很多事,接下来我们浅显的探讨下系统做了哪些工作

理解图片格式

一般我们用的图片大致为.png.jpgwebp几种格式,这些格式其实只是对图片的压缩,你可以理解成一直图片在电脑里面是个二进制数据的集合,但是这个集合很大也不方便给大众传播,所以人们就想了个办法,把数据进行压缩,这样就有了无损压缩和有损压缩,也就对应生成了各种格式

换一种理解方式就是,我们在传输某个文件的时候,往往会把文件压缩成.zip.rar这些格式,图片的压缩其实和这个同理

这样,我们在显示图片的时候,要先把对应的图片解压缩,好比打开.zip的文件时,要先把对应的文件解压到某个文件夹一样,那么在iOS系统里面,是什么时候进行解压的呢?

图片加载的流程

概括来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流如下:

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  2. 然后将生成的 UIImage 赋值给 UIImageView
  3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
    1. 分配内存缓冲区用于管理文件 IO 和解压缩操作;
    2. 将文件数据从磁盘读到内存中;
    3. 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
    4. 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。

位图

图片被解压缩后就变成了位图。什么是位图呢?

A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.

从上面的英文中可以看到,位图其实就是一个像素数组,数组中的每一个像素就代表了图片上的一个点

上面这个图片我们看文件简介是:2859B,尺寸是60*60
我们使用下面的代码:

UIImage *image = [UIImage imageNamed:@"check_green"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
NSData *myData = (NSData *)CFBridgingRelease(rawData);
NSLog(@"%ld",myData.length);

就可以获取到这个图片的原始像素数据,大小为 14400B
这样来看,我们图片的原始大小为14400B,这个大小是怎么得到的呢?
事实上,解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:

解压缩后的图片大小 = 图片的像素宽 60 * 图片的像素高 60 * 每个像素所占的字节数 4

所以,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比

强制解压缩的原理

既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响我们应用的响应性,那么是否有比较好的解决方案呢?答案是肯定的。

我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。

而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate

/* Create a bitmap context. The context draws into a bitmap which is `width'
   pixels wide and `height' pixels high. The number of components for each
   pixel is specified by `space', which may also specify a destination color
   profile. The number of bits for each component of a pixel is specified by
   `bitsPerComponent'. The number of bytes per pixel is equal to
   `(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
   consists of `bytesPerRow' bytes, which must be at least `width * bytes
   per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
   of the number of bytes per pixel. `data', if non-NULL, points to a block
   of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
   data for context is allocated automatically and freed when the context is
   deallocated. `bitmapInfo' specifies whether the bitmap should contain an
   alpha channel and how it's to be generated, along with whether the
   components are floating-point or integer. */
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
    CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

顾名思义,这个函数用于创建一个位图上下文,用来绘制一张宽 width 像素,高 height 像素的位图。这个函数的注释比较长,参数也比较难理解,但是先别着急,我们先来了解下相关的知识,然后再回过头来理解这些参数,就会比较简单了。

  • data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
  • widthheight :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
  • bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
  • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化,更多信息可以查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters? 和 Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? ,亲测可用;
  • space :颜色空间,一般使用 RGB 即可;
  • bitmapInfo :位图的布局信息。

SDWebImage的实现方式

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    // while downloading huge amount of images
    // autorelease the bitmap context
    // and all vars to help system to free memory
    // when there are memory warning.
    // on iOS7, do not forget to call
    // [[SDImageCache sharedImageCache] clearMemory];
    
    if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
        return nil;
    }
    
    @autoreleasepool{
        // do not decode animated images
        if (image.images != nil) {
            return image;
        }
        
        CGImageRef imageRef = image.CGImage;
        
        //在渲染的时候,把图层按像素叠加, 并且会对每一个像素进行 RGBA 的叠加计算。当某个 layer 的是不透明的,GPU 可以直接忽略掉其下方的图层,这就减少了很多工作量。这也是调用 CGBitmapContextCreate 时 bitmapInfo 参数设置为忽略掉 alpha 通道的原因
        
        CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
        BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                         alpha == kCGImageAlphaLast ||
                         alpha == kCGImageAlphaPremultipliedFirst ||
                         alpha == kCGImageAlphaPremultipliedLast);
        if (anyAlpha) {
            return image;
        }
        
        // current
        CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
        CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
        
        BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                      imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                      imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                      imageColorSpaceModel == kCGColorSpaceModelIndexed);
        if (unsupportedColorSpace) {
            colorspaceRef = CGColorSpaceCreateDeviceRGB();
        }
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        NSUInteger bytesPerPixel = 4;
        NSUInteger bytesPerRow = bytesPerPixel * width;
        NSUInteger bitsPerComponent = 8;
        
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     bitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        
        if (unsupportedColorSpace) {
            CGColorSpaceRelease(colorspaceRef);
        }
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(imageRefWithoutAlpha));
        NSData *myData = (NSData *)CFBridgingRelease(rawData);
        NSLog(@"originalImage --- %ld",myData.length);
        
        return imageWithoutAlpha;
    }
}
  • 使用CGBitmapContextCreate函数创建一个位图上下文;
  • 使用CGContextDrawImage函数将原始位图绘制到上下文中;
  • 使用CGBitmapContextCreateImage函数创建一张新的解压缩后的位图;
  • 最后又创建了一个UIImage对象
    SDWebImage中有个不一样的地方在于,如果他检查到你有透明通道,他就会直接返回原图片,而不会进行解压了

下面我们看看其他开源库里面解压缩使用的参数


CGBitmapContextCreate.png

在上表中,用浅绿色背景标记的参数即为我们在前面的分析中所推荐的参数,用这些参数解压缩后的图片渲染的速度会更快。因此,从理论上说 YYKit 中的解压缩算法是三者之中最优的

最后说一点

UIImage *originalImage = [UIImage imageNamed:@"logo"];
    NSLog(@"originalImage --- %ld",UIImageJPEGRepresentation(originalImage, 0).length);

使用上面这种方式来看图片的文件大小很不准确,大家可以试试,如果有好的查看文件大小的方法,可以留言一起探讨下,还是建议大家看我文章开头的那篇文章,比我这篇写的详细

你可能感兴趣的:(iOS中图片的解压缩)