图片的加载流程
参考下图大致的加载流程
图片初始创建是不会解压的,只有在显示前才会去准备解压,这样如果有很多图片同时需要展示就会造成主线任务繁重。
另外在我们展示图片是有时为了降低频繁操作工作量,会选择异步解码图片。主体思路为:在子线程,将原始的图片渲染成新的以字节显示的图片。
代码操作如下(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