版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.02.26 |
前言
我们做APP,文字和图片是绝对不可缺少的元素,特别是图片一般存储在图床里面,一般公司可以委托第三方保存,NB的公司也可以自己存储图片,ios有很多图片加载的第三方框架,其中最优秀的莫过于SDWebImage,它几乎可以满足你所有的需求,用了好几年这个框架,今天想总结一下。感兴趣的可以看其他几篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 图片类型判断深入研究
7. SDWebImage探究(七) —— 深入研究图片下载流程(一)之有关option的位移枚举的说明
8. SDWebImage探究(八) —— 深入研究图片下载流程(二)之开始下载并返回下载结果的总的方法
9. SDWebImage探究(九) —— 深入研究图片下载流程(三)之下载之前的缓存查询操作
10. SDWebImage探究(十) —— 深入研究图片下载流程(四)之查询缓存后的block回调处理
11. SDWebImage探究(十一) —— 深入研究图片下载流程(五)之SDWebImageDownloadToken和操作对象的生成和返回
12. SDWebImage探究(十二) —— 深入研究图片下载流程(六)之下载器到具体下载操作的代理分发实现
13. SDWebImage探究(十三) —— 深入研究图片下载流程(七)之NSURLSession中几个代理的基本用法和关系
14. SDWebImage探究(十四) —— 深入研究图片下载流程(八)之下载完成代理方法的调用
15. SDWebImage探究(十五) —— 深入研究图片下载流程(九)之身份验证质询代理方法调用
16. SDWebImage探究(十六) —— 深入研究图片下载流程(十)之缓存相关代理方法调用
17. SDWebImage探究(十七) —— 深入研究图片下载流程(十一)之收到响应代理方法调用
18. SDWebImage探究(十八) —— 深入研究图片下载流程(十二)之收到图像数据代理方法调用
19. SDWebImage探究(十九) —— 图像的解码 (一)
图像是否需要解码的判断
关于是否可以解码的判断,上一篇已经写很多了,这里就不多说了,直接看上一篇 19. SDWebImage探究(十九) —— 图像的解码 (一)
图像是否需要缩放的判断
下面看一下是否需要缩放的判断,主要对应下面这段代码。
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
BOOL shouldScaleDown = YES;
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
float imageScale = kDestTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
这里实际涉及到的是像素的计算。
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
float imageScale = kDestTotalPixels / sourceTotalPixels;
原像素的总数就是sourceTotalPixels
,这里规定了一个目标像素数目,计算方式如下:
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const size_t kBytesPerPixel = 4;
/*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
定义设置标记“SDWebImageScaleDownLargeImages”时解码图像的最大大小(以MB为单位)
iPad1和iPhone 3GS的建议值:60
iPad2和iPhone 4的建议值:120
iPhone 3G和iPod 2及更早版本设备的建议值:30
static const CGFloat kDestImageSizeMB = 60.0f;
这里可以看到,kDestTotalPixels = 目标图像大小 * 每MB的像素数目 = 60 * 每MB的字节数/每像素的字节数 = 60 * 1024.0f * 1024.0f / 4
,具体多少就不算了,只要下载下来的图像的像素数比这个要大,那么就需要缩放kDestTotalPixels / sourceTotalPixels < 1
,返回YES;否则返回NO,就不需要缩放。
图像的解码和缩放的实现
下面看一下图像的解码和缩放的实现。
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}
if (![UIImage shouldScaleDownImage:image]) {
return [UIImage decodedImageWithImage:image];
}
CGContextRef destContext;
// 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];
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
float imageScale = kDestTotalPixels / sourceTotalPixels;
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
// current color space
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
size_t bytesPerRow = kBytesPerPixel * destResolution.width;
// 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.
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
}
}
下面是代码中用到的一些常量
/*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 60.0f;
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.0f;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
这里,其实就是做下面几个工作:
- 获取图像的缩放比
- 实例化位图上下文
- 获取原色块矩形和目标色块矩形的大小
- 生成图像
下面我们就详细的看一下解码和缩放的实现。
1. 获取图像的缩放比
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
float imageScale = kDestTotalPixels / sourceTotalPixels;
这个获取图像的缩放比,其实和是否需要进行缩放里面的判断是类似的,都是计算图像的像素数目和指定的目标像素数目进行比较imageScale = kDestTotalPixels / sourceTotalPixels
,这个是一个大于0小于1的数字。
接下里就是获取目标图像尺寸,其实就是原图像的宽高乘以缩放比。
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
2. 实例化位图上下文
这里实例化位图上下文的方式和上一篇中示例的方式是一样的,就不多说了。
(a) 容错处理
首先进行了容错处理。
if (destContext == NULL) {
return image;
}
如果实例化失败,那么直接返回传入的原图像。
(b) 设置上下文的插值质量
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
我们首先看一下这个函数
/* Set the interpolation quality of `context' to `quality'. */
CG_EXTERN void CGContextSetInterpolationQuality(CGContextRef cg_nullable c,
CGInterpolationQuality quality)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
这里插值质量的参数是一个枚举,如下所示:
/* Interpolation quality. */
typedef CF_ENUM (int32_t, CGInterpolationQuality) {
kCGInterpolationDefault = 0, /* Let the context decide. */
kCGInterpolationNone = 1, /* Never interpolate. */
kCGInterpolationLow = 2, /* Low quality, fast interpolation. */
kCGInterpolationMedium = 4, /* Medium quality, slower than kCGInterpolationLow. */
kCGInterpolationHigh = 3 /* Highest quality, slower than kCGInterpolationMedium. */
};
CGContextSetInterpolationQuality
允许上下文在各个保真度等级插入像素。
3. 获取原色块矩形和目标色块矩形的大小
现在定义用于从输入图像到输出图像的增量色块的矩形的大小。 由于iOS从磁盘检索图像数据的方式,我们使用与源图像宽度相等的源图像宽度。 即使当前图形上下文在该范围内被裁剪为子矩形,iOS也必须从全宽度“范围”解码磁盘上的图像。 因此,我们通过将我们的图块大小修改为输入图像的整个宽度,充分利用解码操作产生的所有像素数据。
(a) 计算原色块矩形和目标色块矩形大小
主要就是对应下边这段代码
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
(b) 计算组成输出图像所需要的读写操作
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
4. 生成图像
有了前面的计算,生成图像就很简单了,主要就是对应下边这几句代码。
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
这里仍有两处容错处理,采用的生成方法就是
+ (UIImage *)imageWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(4_0);
后记
本篇文章主要是对图像进行解码和缩放。首先获取图像的缩放比,实例化位图上下文,然后获取原色块矩形和目标色块矩形的大小,最后生成图像。