图片加载过程及代码优化方案

图片的加载流程

参考下图大致的加载流程


图片操作流程.png

图片初始创建是不会解压的,只有在显示前才会去准备解压,这样如果有很多图片同时需要展示就会造成主线任务繁重。

另外在我们展示图片是有时为了降低频繁操作工作量,会选择异步解码图片。主体思路为:在子线程,将原始的图片渲染成新的以字节显示的图片。

代码操作如下(SDWebImage解决方案),其本质是:

在子线程直接通过新创建一张位图的方式绘制一张图片。此时就已经完成了解压操作,可以后续直接使用。

// 以下代码需要自己操作异步方案处理
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (image.images) {
        // Do not decode animated images
        return image;
    }
    
    CGImageRef imageRef = image.CGImage;
    CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize};
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
    
    int infoMask = (bitmapInfo & kCGBitmapAlphaInfoMask);
    BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone ||
                        infoMask == kCGImageAlphaNoneSkipFirst ||
                        infoMask == kCGImageAlphaNoneSkipLast);
    
    // CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB.
    // https://developer.apple.com/library/mac/#qa/qa1037/_index.html
    if (infoMask == kCGImageAlphaNone && CGColorSpaceGetNumberOfComponents(colorSpace) > 1) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;
        
        // Set noneSkipFirst.
        bitmapInfo |= kCGImageAlphaNoneSkipFirst;
    }
    // Some PNGs tell us they have alpha but only 3 components. Odd.
    else if (!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace) == 3) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;
        bitmapInfo |= kCGImageAlphaPremultipliedFirst;
    }
    
    // It calculates the bytes-per-row based on the bitsPerComponent and width arguments.
    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 imageSize.width,
                                                 imageSize.height,
                                                 CGImageGetBitsPerComponent(imageRef),
                                                 0,
                                                 colorSpace,
                                                 bitmapInfo);
    CGColorSpaceRelease(colorSpace);
    
    // If failed, return undecompressed image
    if (!context) return image;
    
    CGContextDrawImage(context, imageRect, imageRef);
    CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);
    
    CGContextRelease(context);
    
    UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(decompressedImageRef);
    return decompressedImage;
}


绘制一个纯色的图片

  • 使用UIGraphicsImageRenderer进行绘制,内部会走CoreImage这一套

    推荐使用这种方式,拥有缓存机制使用更加方便,不用管理上下文。iOS10后可用

    UIGraphicsImageRenderer *render = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(100, 100)];  // 指定渲染区域大小
    UIImage *img = [render imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
        [UIColor.blueColor setFill];
        [rendererContext fillRect:CGRectMake(0, 0, 100, 100)];
    }];
  • 使用Quartz2D绘制
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, UIScreen.mainScreen.scale);
    [UIColor.redColor setFill];
    CGContextFillRect(UIGraphicsGetCurrentContext(),CGRectMake(0, 0, 100, 100));
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(10, 3, 100, 100)];
    imgV.image = image;
    [self.view addSubview:imgV];

压缩大图至指定尺寸

  • 使用以下方案,会有内存峰值出现

- (void)testDrawWithRender {
    
    UIImage *image = [UIImage imageNamed:@"zz"];
    
    UIGraphicsImageRenderer *render = [[UIGraphicsImageRenderer alloc] initWithSize:self.renderSize];
    UIImage *img = [render imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
        [image drawInRect:CGRectMake(0, 0, self.renderSize.width, self.renderSize.height)];
    }];
    
    UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(10, 3, self.renderSize.width, self.renderSize.height)];
    imgV.image = img;
    [self.view addSubview:imgV];
}

  • 使用ImageIO渲染出一张缩略图

    ImageIO能够在不产生dirty memory的情况下读取到图片尺寸和元数据信息,其内存损耗等于缩减后的图片尺寸产生的内存占用。


- (void)testDrawWithImageIO {
    
    NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"zz" ofType:@"png"]];
    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    
//  UIImage *image = [UIImage imageNamed:@"zz.png"];        // 如果使用了这种方式,那么大图一定会加载到内存中,会有峰值出现
//  NSData *data = UIImagePNGRepresentation(image);
//  CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    
    CFDictionaryRef options = (__bridge CFDictionaryRef)@{
        (id)kCGImageSourceCreateThumbnailFromImageIfAbsent:@(YES),
        (id)kCGImageSourceThumbnailMaxPixelSize:@100,   // 最大像素,如果设置很小就不清楚
        (id)kCGImageSourceShouldCache:@YES
    };
    
    // 生成缩略图
    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSourceRef, 0, options);
    UIImage *img = [UIImage imageWithCGImage:imageRef];
    
    // 释放内存
    CGImageRelease(imageRef);
    CFRelease(imageSourceRef);
    
    UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(10, 3, self.renderSize.width, self.renderSize.height)];
    imgV.image = img;
    [self.view addSubview:imgV];
    
}

常用枚举属性解析析:

  • kCGImageSourceThumbnailMaxPixelSize

    设置缩略图最大像素值,图片尺寸不会发生改变

  • kCGImageSourceCreateThumbnailFromImageIfAbsent

    如果图像源文件中不存在缩略图,是否应自动为图像创建缩略图。

    缩略图是根据完整图像创建的,受kCGImageSourceThumbnailMaxPixelSize指定的限制。

    如果未指定最大像素大小,则缩略图是完整图像的大小,这在大多数情况下是不可取的。此键必须是CFBoolean值。默认值为kCFBooleanFalse

    可以在传递给函数CGImageSourceCreateThumbnailAtIndex的选项字典中提供此键。

  • kCGImageSourceCreateThumbnailFromImageAlways

    是否应根据完整图像创建缩略图,即使图像源文件中存在缩略图。

  • kCGImageSourceCreateThumbnailWithTransform

    缩略图是否应根据全图像的方向和像素纵横比进行旋转和缩放。此键的值必须是CFBoolean值。默认值为kCFBooleanFalse

  • kCGImageSourceShouldCache

    图像是否应以解码形式缓存


总结

通过了解图片加载机制,可以对后续优化方向和理解一些框架设计思路有很大的帮助。
本文记录些代码操作都是工作中常用的方案(复制可用),在此也作为一个留档。


参考:

https://segmentfault.com/a/1190000002776279

https://blog.jamchenjun.com/2018/08/22/image-and-graphics-best-practices.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。