SDWebImage的图片解码源码阅读
SDWebImage中对下载完的图片在子线程解码后才放到ImageView中显示,这避免了系统在主线程解码而导致的卡顿问题,本文主要解读图片解码时做了哪些事情及如何显示gif动态图和Webp格式图片。上一篇也值得阅读SDWebImage的源码阅读
一、图片解码的过程
在我们设置JPG/PNG
图片给UIImageView
后,系统在将图片渲染到屏幕上时会默认在主线程完成解码、图片重采样两个操作:
解码的原因:
显示到屏幕中的图像是位图图像, 而JPG或PNG图片是编码压缩后的图片格式,在显示到屏幕之前,需要解码成位图图像 ,位图图像的大小与图片的宽高像素有关,假设一个3MB的图片,其宽高像素为2048 * 2048的图片,解码后的位图图像大小是16MB(2048 * 2048 * 4)。
图片重采样:
图片大小和imageView大小不一致时,系统根据周围像素点按一定权重获得目标图像像素点的值,SDWebImage中下载的图片,即使解码缩放, 图片大小未必和imageView的大小相同,这会引发重采样,我们可以在图片显示前,将图片裁剪成和imageView的大小相同,提升性能。
如果一个页面图片比较多,为了保证流畅度需要手动在子线程完成图片解码和裁剪工作,SDWebImage就是这样做的,下面是解码工作的细节:
- 1. 网络加载图片成功时会调用这里进行图片处理
UIImage * SDImageLoaderDecodeImageData(NSData * imageData, NSURL * imageURL, SDWebImageOptions options, SDWebImageContext * context) {
if (!decodeFirstFrame) {
//1. 如果不是设置的只解码第一帧图片,尝试用动态图`SDAnimatedImage`,下面的代码是追踪后简写的
image = [[SDAnimatedImage alloc] initWithData:imageData scale:scale options:coderOptions];
[((id<SDAnimatedImage>)image) preloadAllFrames];
}
if (!image) {
// 3.如果不是动态图,则使用SDImageCodersManager中包含的解码类对imageData转成image
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
}
// 4.解码工作
if (shouldDecode) {
BOOL shouldScaleDown = options & SDWebImageScaleDownLargeImages;
if (shouldScaleDown) {// 将原图按照固定大小分割,然后依次绘制到目标画布
image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
} else {// 直接解码到位图生成新图
image = [SDImageCoderHelper decodedImageWithImage:image];
}
}
}
- 2. SDImageCodersManager类将imageData转为image的细节
- (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)options {
UIImage *image;
// 默认self.coders =[SDImageIOCoder, SDImageGIFCoder, SDImageAPNGCoder]
NSArray<id<SDImageCoder>> *coders = self.coders;
for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
image = [coder decodedImageWithData:data options:options];
break;
}
}
return image;
}
// 1.SDImageIOCoder :canDecodeFromData: PNG, JPEG, TIFF; GIF的第一帧;(ios 11 A9以上仿生芯片)的HEIC\HEIF
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
UIImage *image = [[UIImage alloc] initWithData:data scale:scale];
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
return image;
}
// 2.SDImageGIFCoder: 仅支持GIF动态图解码
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (decodeFirstFrame || count <= 1) {// 1.设置的只解码第一帧
animatedImage = [[UIImage alloc] initWithData:data scale:scale];
} else {
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
for (size_t i = 0; i < count; i++) {
// 2.一帧一帧转化为UIImage *image,并封装为SDImageFrame *frame
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
[frames addObject:frame];
}
animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
}
}
// SDImageCoderHelper类中转化为动态图的细节
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
UIImage *animatedImage;
NSUInteger durations[frameCount];
for (size_t i = 0; i < frameCount; i++) {
durations[i] = frames[i].duration * 1000;
}
NSUInteger const gcd = gcdArray(frameCount, durations);
__block NSUInteger totalDuration = 0;
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
UIImage *image = frame.image;
NSUInteger duration = frame.duration * 1000;
totalDuration += duration;
NSUInteger repeatCount;
if (gcd) {
repeatCount = duration / gcd;
} else {
repeatCount = 1;
}
for (size_t i = 0; i < repeatCount; ++i) {
[animatedImages addObject:image];
}
}];
animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
}
// SDImageAPNGCoder: 只支持PNG图片格式的解码(内容是动态图!!): 于GIF的转成UIImage方式是一样的,这里不展示了
- 3.普通图片解码
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
// 1. 创建位图上下文
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
// 2. 转换图片的方向
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
//3. 将图片绘制到上下文中,生成图片
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
return newImageRef;
}
- 4.超大图的缩放解码:设置了SDWebImageScaleDownLargeImages才会进入到这里
// 先过滤掉不符合解码条件,位图大小不达标的(小于60MB);再将原图按照固定大小分割,然后依次绘制到目标画布上
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
BOOL shouldScaleDown = YES;
//1.图片的总像素为0,不用缩放
if (sourceTotalPixels <= 0) {
return NO;
}
// 2. 缩放目标总像素小于1MB拥有到像素,不用缩放
if (destTotalPixels <= kPixelsPerMB) {
// Too small to scale down
return NO;
}
// 3. 目标总像素比图片像素点更大,不用缩放
float imageScale = destTotalPixels / sourceTotalPixels;
if (imageScale > 1) {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
// 1. 判断如果不需要缩放则直接按普通图片方式解码
if (![self shouldScaleDownImage:image limitBytes:bytes]) {
return [self decodedImageWithImage:image];
}
CGFloat destTotalPixels = kDestTotalPixels;// 设置的是目标总像素==》60MB拥有的像素
CGFloat tileTotalPixels = kTileTotalPixels;// 设置的是每一小片20MB拥有的像素点
@autoreleasepool {
//1. 图片sourceImageRef拥有的总像素点
CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
//2. 目标总像素与图片总像素比值的平方根作为缩放比例,根据比例算出目标图片宽高
CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
destResolution.width = (int)(sourceResolution.width * imageScale);
destResolution.height = (int)(sourceResolution.height * imageScale);
//3. 颜色空间device color space
CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
//4. 创建图形上下文
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
//5. 设置图像插值的质量为高,设置图形上下文的插值质量水平允许上下文以各种保真度水平内插像素。
// 在这种情况下,kCGInterpolationHigh通过最佳结果
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
//6. 计算源图片一小片的宽高
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
//7. 计算目标一小片的宽高
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
// 8. 计算源图像与压缩后目标图像重叠的像素大小,kDestSeemOverlap=2.f
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
// 9.根据图片总高度/每一小片高度,计算出总共行数iterations
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// 10. 根据总行数从源图像中一小块一小块画到目标图形上下文中destContext
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
// 11. 从源图像中取出一小块
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
// 12.将取出的一小块sourceTileImageRef绘制到上下文destContext的destTile小块中
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
// 13.从目标上下文生成新图片并返回
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
return destImage;
}
}
图片解码可能会带来的问题:
在处理高分辨率大图时直接解码操作会让内存暴增, 所以在处理这个情况可以使用
//SDWebImageOptions选择SDWebImageScaleDownLargeImages,处理网络高分辨率图
[self.imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageScaleDownLargeImages];
SDWebImage默认支持的图片解码:
/// SDImageCodersManager类中
- (instancetype)init {
if (self = [super init]) {
// initialize with default coders
_imageCoders = [NSMutableArray arrayWithArray:@[
[SDImageIOCoder sharedCoder], // PNG, JPEG, TIFF; GIF的第一帧;(ios 11 A9以上仿生芯片)的HEIC\HEIF
[SDImageGIFCoder sharedCoder], // GIF
[SDImageAPNGCoder sharedCoder]// APNG
]];
_codersLock = dispatch_semaphore_create(1);
}
return self;
}
二、加载的如果是gif动态图时如何实现
加载显示动态图有两个选择:SDAnimatedImageView
、另一个库FLAnimatedImage
。
SDAnimatedImageView
:在使用SDWebImage加载GIF,当滑动加载更多的cell时, 内存暴涨。
FLAnimatedImage
:是比较轻量级的库,支持可变帧间延时、内存内存表现良好、播放流畅等特点,推荐使用。
FLAnimatedImage源码剖析
三、webp格式图片怎么显示
webp是谷歌创造的一种图片格式,webp图片在无损压缩的情况下,比png要小28%左右,图片质量上跟png差不多, 所以出于性能考虑可能会选择webp图片作为网络图片。
webp格式图片的使用:在github下载YYWebImage
将YYWebImage
中的WebP.framework
导入工程,使用时与普通图片一样,如果是动态的webp图片推荐使用YYAnimatedImageView
去显示。
具体做法参考:
iOS SDWebImage加载webp图片
SDWebImage 加载显示 WebP 与性能问题
相关比较好的文章:
SDWebImage源码看图片解码
绘制像素到屏幕的过程