OC:image的加载.解压.渲染绘制(Quartz异步绘制图片

2018-09-07  本文已影响0人  stonly916

一、关于图片的两种格式,PNG和JPEG

图片文件被加载后必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。

用于加载的CPU时间相对于解码来说根据图片格式而不同。对于iOS来说大多处理的就是PNG和JPEG了。

当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。这就会在准备绘制图片的时候影响性能,因为需要在绘制之前进行解压(通常是消耗时间的问题所在)。

二、图片的加载

图片的加载的几种方法,这里只说两种:

在说图片的解压和渲染绘制之前,我们先说下CoreGraphics中的一些我们要用到的方法等。

1.Graphics Context(图形上下文)类型:

2.坐标系:
UIKit默认是左上角零点,y向下的坐标系。
而Quartz是默认左下角零点,y向上的坐标系。

一般情况下,对于我们经常使用的Bitmap Context,如果其对应一个视图,视图在屏幕上的就是y下坐标系,在屏幕外的是y上坐标系。

日常使用中需要注意:

3.再来看CGImage的创建

CGImageRef __nullable CGImageCreate(
  size_t width, size_t height,

  size_t bitsPerComponent, 
  size_t bitsPerPixel, 
  size_t bytesPerRow,
 
  CGColorSpaceRef cg_nullable space, 
  CGBitmapInfo bitmapInfo,
  CGDataProviderRef cg_nullable provider,

  const CGFloat * __nullable decode, 
  bool shouldInterpolate,
 CGColorRenderingIntent intent
)CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

参数真的是多,一个一个看:

CGImageAlphaInfo透明度分量信息和CGBitmapInfo位图布局信息:

typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {

 kCGImageAlphaNone,  /* For example, RGB. */
 kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */
 kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
 kCGImageAlphaLast, /* For example, non-premultiplied RGBA */
 kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
 kCGImageAlphaNoneSkipLast,  /* For example, RBGX. */
 kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */
 kCGImageAlphaOnly /* No color data, alpha data only */
};

typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
 kCGBitmapAlphaInfoMask = 0x1F,
 kCGBitmapFloatInfoMask = 0xF00,
 kCGBitmapFloatComponents = (1 << 8),
 kCGBitmapByteOrderMask  = kCGImageByteOrderMask,
 kCGBitmapByteOrderDefault = kCGImageByteOrderDefault,
 kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
 kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
 kCGBitmapByteOrder16Big = kCGImageByteOrder16Big,
 kCGBitmapByteOrder32Big = kCGImageByteOrder32Big
}

#ifdef __BIG_ENDIAN__
//[大端序](https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F#.E5.A4.A7.E7.AB.AF.E5.BA.8F)
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
//小端序
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif

CGColorRenderingIntent绘制意图:

/* Color rendering intents. */

typedef CF_ENUM (int32_t, CGColorRenderingIntent) {
 kCGRenderingIntentDefault,
 kCGRenderingIntentAbsoluteColorimetric,
 kCGRenderingIntentRelativeColorimetric,
 kCGRenderingIntentPerceptual,
 kCGRenderingIntentSaturation
};

4.CGBitmapContext的创建:

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(10.0, 2.0);

在之前介绍过的参数就不详细说明了。

颜色空间space

下图是苹果支持的颜色空间和像素格式:


屏幕快照 2018-09-07 上午10.48.08.png

颜色空间iOS支持上述的其中8种, iOS不支持与设备无关或通用的颜色空间。根据Quartz 2D的文档所说,iOS应用程序必须使用设备颜色空间

设备颜色空间主要由iOS应用程序使用,因为其他选项不可用。在大多数情况下,Mac OS X应用程序应使用通用颜色空间,而不是创建设备颜色空间,但是,一些Quartz例程要求具有设备颜色空间的图像,例如,如果调用CGImageCreateWithMask并指定图像作为蒙版,则必须使用设备灰色颜色空间定义图像。

所以iOS中,我们通常使用CGColorSpaceCreateDeviceRGB()来创建颜色空间,还有bitsPerComponent和btyesPerRow要和颜色空间匹配。

三、图片的解压

check_green.png

这是一张30x30的图,存储大小843B,

UIImage *image = [UIImage imageNamed:@"check_green.png"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));

上面的代码可以获得图片解压后的大小,为3600B。解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:
解压缩后的图片大小 = 图片的像素宽 30 * 图片的像素高 30 * 每个像素所占的字节数 4

使用+imageNamed:方法进行加载并解压图片,其实内部就是使用的ImageIO框架。

1.使用ImageIO库来加载本地image:使用kCGImageSourceShouldCache来创建图片,强制图片立刻解压缓存,然后在图片的生命周期保留解压后的版本。

    NSString *str = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/image"];

    CGDataProviderRef provider = CGDataProviderCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:str]);
    NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source1, 0,(__bridge CFDictionaryRef)options);
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    CFRelease(source);
绘制图片过程中,系统会对图片自动解压,所以绘制图片也算是一种解压的方式。

2.使用UIGraphicsBeginImageContext进行绘制图片,创建一个基于bitmap的imageContext,并把它设置成为当前正在使用的context,然后将图片绘制到context上。这方法在需要大量调用的时候不要用,因为是系统释放内存,所以存在延迟释放的情况,短时间内大量调用(如tableView的滑动异步绘制图片)会出现内存暴涨的情况:

    //创建一个基于bitmap的imageContext,并把位图立即推入到图形上下文堆栈中。坐标系是UIKit默认坐标系左上角0点y下。
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(160, 220), YES, [UIScreen 
mainScreen].scale);
    
    //这里要记住CGContextDrawImage会默认使用离屏bitmap的坐标系即左下角0点y上,所以要变换坐标。
//CGContextRef ctx = UIGraphicsGetCurrentContext();
    //CGContextTranslateCTM(ctx, 0, 220);
    //CGContextScaleCTM(ctx, 1.0, -1.0);
    //CGContextDrawImage(ctx, CGRectMake(0, 0, 160, 220), img.CGImage);
    [image drawInRect:CGRectMake(0, 0, width, height)];

    UIGraphicsEndImageContext();

3.使用CGBitmapContextCreate绘制图片,同方法1中绘制比较,根据官方文档说明,因为bitmap context是左下角y上坐标系,如果实际绘画中需要转换坐标系,不一定比imagContext绘制快。但是我们其实大部分都是在屏幕外绘制图片的,所以不需要变换坐标,实际证明比方法UIGraphicsBeginImageContext绘制要快。

官方文档上示例代码:

//Creating a bitmap graphics context
    CGImageRef imgRef = image.CGImage;
    size_t pixelsWide = CGImageGetWidth(imgRef);
    size_t pixelsHigh = CGImageGetHeight(imgRef);
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;
    
    bitmapBytesPerRow   = (pixelsWide * 4);
    bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);
    
    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    void *          bitmapData;
    bitmapData = calloc( bitmapByteCount, sizeof(uint8_t) );
    if (bitmapData == NULL) {
        fprintf (stderr, "Memory not allocated!");
        return nil;
    }
    context = CGBitmapContextCreate (bitmapData,
                                     pixelsWide,
                                     pixelsHigh,
                                     8,      // bits per component
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     kCGImageAlphaPremultipliedLast);
    if (context == NULL) {
        free (bitmapData);
        fprintf (stderr, "Context not created!");
        return NULL;
    }
    CGColorSpaceRelease( colorSpace );
    return context;

//Drawing to a bitmap graphics context
    CGRect myBoundingBox;// 1
    myBoundingBox = CGRectMake (0, 0, myWidth, myHeight);// 2
    myBitmapContext = MyCreateBitmapContext (400, 300);// 3
    // ********** Your drawing code here ********** // 4
    CGContextSetRGBFillColor (myBitmapContext, 1, 0, 0, 1);
    CGContextFillRect (myBitmapContext, CGRectMake (0, 0, 200, 100 ));
    CGContextSetRGBFillColor (myBitmapContext, 0, 0, 1, .5);
    CGContextFillRect (myBitmapContext, CGRectMake (0, 0, 100, 200 ));
    myImage = CGBitmapContextCreateImage (myBitmapContext);// 5
    CGContextDrawImage(myContext, myBoundingBox, myImage);// 6
    char *bitmapData = CGBitmapContextGetData(myBitmapContext); // 7
    CGContextRelease (myBitmapContext);// 8
    if (bitmapData) free(bitmapData); // 9
    CGImageRelease(myImage);

上面的代码用到了bitmapData,一定要记得手动释放。实际使用的时候,我们一般data输入NULL就好,bitmapBytesPerRow设置为0,这样系统会自动为我们分配位图存储内存,并自动计算每行字节数。

下面是我们通常使用bitmap context来绘制图片的代码:

+ (UIImage *)imageDecodeByBitmap:(UIImage *)image
{
    //创建一个bitmap的context,并把它设置成为当前正在使用的context
    
    CGImageRef imgRef = image.CGImage;
    size_t width = CGImageGetWidth(imgRef);
    size_t height = CGImageGetHeight(imgRef);
    
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imgRef);

    BOOL hasAlpha = NO;
    if (alphaInfo == kCGImageAlphaPremultipliedLast ||
        alphaInfo == kCGImageAlphaPremultipliedFirst ||
        alphaInfo == kCGImageAlphaLast ||
        alphaInfo == kCGImageAlphaFirst) {
        hasAlpha = YES;
    }
    CGBitmapInfo info = kCGBitmapByteOrder32Host | (hasAlpha ? kCGImageAlphaPremultipliedFirst: kCGImageAlphaNoneSkipFirst);
    //创建一个bitmap的context
    CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, 8, 0, SharedCGColorSpaceGetDeviceRGB(), info);
    
    CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), imgRef);
    
    CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
    CGContextRelease(ctx);
    
    image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    
    return image;
}

4.使用CGDataProviderCopyData解压,然后使用CGImageCreate重新创建解压后的图片。

    CGImageRef imageRef = image.CGImage;
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
    size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
    size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
    size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

    CGDataProviderRef provider = CGImageGetDataProvider(imageRef);
    CFDataRef rawData = CGDataProviderCopyData(provider); //解压
    provider = CGDataProviderCreateWithCFData(rawData);
    CFRelease(rawData);

    imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, NULL, false, kCGRenderingIntentDefault);

    CGDataProviderRelease(provider);
    UIImage *newImage = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);

这是一个单纯的解压方法,不像前面几个方法都是绘制图片,不过这个方法也有个瑕疵,在使用过程中,会出现内存不断增加的情况:进入这个视图控制器内存增加一些,pop出去后再进来又增加一些,不断增长,可能从原本的100M内存增加到800M,然后就会回落到100M,猜测可能是系统没有释放位图数据缓存。

四、图片的渲染

正常我们在使用imageView.image = image;的时候,在屏幕显示的时候,GPU需要绘制图片,绘制就要渲染图片,然后才能显示出来。当然我们这里说的渲染图片是提前渲染和异步渲染。

1.把整张图片绘制到CGContext中,然后获取到context中的newImage并保存下来,原图片就可以不用了,加载的时候需要加载newImage,这里代码就是在解压栏目里我讲的“绘制图片”代码。
这种方法多在tableView和collectionView上使用,异步绘制图片然后回到主线程中加载,可以使滑动非常的流畅,这种操作主要是把CPU的同步解压操作换成异步操作,并且把GPU的渲染换成了CPU的离屏渲染,具体方法在“解压图片”栏目中给出的2、3两点。

2.把整个图片只绘制到一个像素大小的bitmap Context中。

    CGImageRef cgImage = image.CGImage;
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    if (width == 0 || height == 0) return;
    
    size_t bitsPerComponent = 8;
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
    BOOL hasAlpha = NO;
    if (alphaInfo == kCGImageAlphaPremultipliedLast ||
        alphaInfo == kCGImageAlphaPremultipliedFirst ||
        alphaInfo == kCGImageAlphaLast ||
        alphaInfo == kCGImageAlphaFirst) {
        hasAlpha = YES;
    }
    CGBitmapInfo info = kCGBitmapByteOrder32Host | (hasAlpha ? kCGImageAlphaPremultipliedFirst: kCGImageAlphaNoneSkipFirst);
    
    CGContextRef context = CGBitmapContextCreate(NULL, 1, 1, bitsPerComponent, 0, SharedCGColorSpaceGetDeviceRGB(), info);
    //解压渲染,但是没有优化
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
    CGContextRelease(context);

这样解压整张图片并提前渲染,好处是只绘制了一个像素,绘制没有消耗任何时间。这样没有解压后的图片,只能加载原图,解压数据和渲染数据都在系统缓存中。
这种方法一般用于静态页面中加载图片。正因为只加载原图,导致原图如果被释放,解压数据等在缓存中也会释放,而且正因为依赖于缓存,如果缓存被释放,那么这个方法相当于无效操作。

实际使用中,

这个demo是写这边文章时写的AsynDisplayImage,有兴趣可以看下。

上一篇 下一篇

猜你喜欢

热点阅读