SDWebImage是作为iOS开发肯定会使用的一个框架,关于它的原理和介绍的文章也看了不少,但是始终模模糊糊,不能形成系统的知识体系,果然只有自己DEBUG过一遍才能印象深刻。
正篇
图片编码解码
A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.
平时使用的PNG、JPEG等格式的图片是经过一定算法编码压缩后的位图,UIImage使用时需要解压,而系统默认解码工作在主线程,所以在大量加载图片的时候会有卡顿就是这个原因,但是具体卡在哪里需要对哪里优化还是需要了解图片加载的过程,在这之前先简单捋一遍需要了解的前置知识。(下面阐述并不一定保证正确)
平时通过操作UIView
的addSubView、insertSubView
建立视图之间的联系,这种关系用一个树的结构保存起来称为视图树
。
每一个UIview
都持有一个CALayer
实例,也就是所谓的(支持图层)backing layer
,UIView
的职责就是创建并管理这个图层,平时使用的frame、bounds
这些属性只是对CALayer
的一层封装。而图层与图层之间的联系也有个树结构叫图层树
。
CALayer
和UIView
最大的不同是CALayer
并不清楚具体的响应链(iOS通过视图层级关系用来传送触摸事件的机制),即使它提供了一些方法来判断是否一个触点在图层的范围之内。
Core Animation
是iOS和OS X上可用的图形渲染和动画基础框架,从图像可以看出Core Animation
负责把上层需要显示的图像做了某些处理过渡到下层渲染引擎。
当改变一个图层的属性,属性值的确是立刻更新的,但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,只是定义了图层动画结束之后将要变化的外观。
Core Animation
扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。
这意味着CALayer
除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。事实上每个图层属性的显示值都被存储在一个叫做呈现图层(presentationLayer)
的独立图层当中,这个呈现图层实际上是模型图层(modelLayer)[我们改变CALayer的各种属性时实际上就是modelLayer的]
的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
图层树中所有图层的呈现图层之间的树关系称为呈现树
。
Core Animation
把需要渲染的图层和数据是打包一个专门的进程进行渲染,叫渲染进程(Render Server)
这整个流程大致分为4个阶段:
- 布局 - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
- 显示 - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的
-drawRect:
和-drawLayer:inContext:
方法的调用路径。 - 准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
- 提交 - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过
IPC(内部处理通信)
发送到渲染服务进行显示。
这是事情是发生在APP内,但是我们只能控制前两个阶段。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树
的图层树。
一张图片的加载过程主要是加载->解码->渲染
,对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。
平时使用加载图片的方式主要是imageNamed
(加载Assets图片)和imageWithContentsOfFile(加载Bundle图片)
,使用imageNamed
加载图片时系统会立刻进行解码并且对解码后的数据进行缓存;而使用imageWithContentsOfFile
使用ImageIO创建CGImageRef
内存映射数据,通过隐式CATransaction
捕获这些层树修改,在主运行循环的下一次迭代中,Core Animation提交隐式事务,这可能涉及创建已设置为层内容的任何图像的副本。根据图像,复制它涉及以下部分或全部步骤:
1. 缓冲区被分配用于管理文件IO和解压缩操作。
2. 文件数据从磁盘读入内存。
3. 压缩的图像数据被解码成其未压缩的位图形式,这通常是CPU密集型操作。
4. 然后,Core Animation使用未压缩的位图数据来渲染图层。
下面做了个测验:
UIImage *img = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image.png" ofType:nil]];
UIImageView *v = [[UIImageView alloc] initWithFrame:self.view.bounds];
v.image = img;
[self.view addSubview:v];
用Time Profile查看结果
在下一次runloop循环迭代里面的
CA:Layer:prepare_commit
也就是prepare阶段中的png_read_IDAT_dataApple
操作耗时,所以猜测是在这里面进行解码,这个过程是在主线程,而在SDWebImage中,当图片下载完成后就立刻在后台线程进行解码,主要是图片绘制到 CGBitmapContext
,利用位图创建图片。
SDWebImageImageIOCoder
SDWebImageCoder
是提供自定义图像解码/编码的图像编码器的协议;SDWebImageProgressiveCoder
继承于SDWebImageCoder
,提供自定义的渐进图像解码的协议, 可以我们自己实现也可以使用默认自带解码器,自带解码器的工作是由
SDWebImageImageIOCoder
、SDWebImageGIFCoder
和SDWebImageWebPCoder
提供, 在这里主要看SDWebImageImageIOCoder
的几个核心方法。
sd_decompressedImageWithImage
static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
//image为nil或者image为动图时不需解码
if (![[self class] shouldDecodeImage:image]) {
return image;
}
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);//判断是否含有alpha通道
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;//小端模式32位主机字节模式[1]
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;//通过判断image是否含有alpha通道来使用不同的位图布局信息[2]
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// CGBitmapContextCreate不支持kCGImageAlphaNone.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);//[3]
if (context == NULL) {
return image;
}
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];//[4]
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
-
kCGBitmapByteOrder32Host
这个宏完整定义为
#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
Intel x86处理器存储一个双字节整数,其中最低有效字节优先,后跟最高有效字节。这称为little-endian(小端序)字节排序。其他CPU(例如PowerPC CPU)存储一个双字节整数,其最高有效字节优先,后跟最低有效字节。这称为big-endian(大端序)字节排序。
通俗来说大端序是指数据的高字节保存在内存的低地址中,数据的低字节,保存在内存的高地址中。小端序则相反,数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址中。
iPhone设备使用的是32位小端模式,使用这个宏可以避免硬编码。
可以看的出来在32bits下RGBA每个通道是8位,即使上面的
kBitsPerComponent
;
- CGBitmapContextCreate创建bitmap上下文
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)
data
:指向要呈现绘图的内存中的目标的指针。这个内存块的大小至少应该是(bytesPerRow*height)字节。当它为NULL时,系统会自动分配内存;
bitsPerComponent
:即上面的kBitsPerComponent
;
bytesPerRow
:位图的每一行要使用的内存字节数。如果数据参数为NULL,则传递一个值0会自动计算该值;之前的版本中使用的是kBytesPerPixel
即每行像素4bytes,但是现在根据苹果文档推荐已经改为0让系统自动计算,而且还会进行cache line alignment的优化;
space
:颜色空间即使用的是RGB;
bitmapinfo
:位图布局信息;
- 虽然名字还是叫
imageWithoutAhlpa
,但是在这次commit中已经修复成不管图片是否带alpha通道都不会成为影响强制解码的因素了(截止今天为止是这样的),所以说解码出来的图片不是一定不带alpha通道的图片。
sd_decompressedAndScaledDownImageWithImage
sd_decompressedAndScaledDownImageWithImage
这个方法中是在解压缩的基础上加个缩放的功能,为了理解这个鬼玩意我把这个方法拿出来DEBUG了一遍,sourceImageRef是一张宽7033px高10110px大小为8.3M的图片;
static const size_t kBytesPerPixel = 4;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestImageSizeMB = 60.0f;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kSourceImageTileSizeMB = 20.0f;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image {
...//判断image
CGContextRef destContext;
@autoreleasepool {
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;
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
... get color space & get bitmapinfo
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (destContext == NULL) {
return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;。
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
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);
... create image & release
return destImage;
}
}
step1 先了解各种常量
kBytesPerMB
:每MB=1024 * 1024 bytes
kPixelsPerMB
: 每MB=1024 * 1024 / 4 = 262144像素
kDestImageSizeMB
:目标图片60MB
kDestTotalPixels
:目标像素=60 * 262144
kSourceImageTileSizeMB
:定义用于解码图像的“块”的最大大小(以MB为单位);
kTileTotalPixels
:每“块”所占的像素;
kDestSeemOverlap
:重叠的“块”的像素数量step2 DEBUG
CGImageRef sourceImageRef = image.CGImage;
<po sourceImageRef <CGImage 0x7fb628d00aa0> (IP) <<CGColorSpace 0x600001f66880> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1)> width = 7033, height = 10110, bpc = 8, bpp = 32, row bytes = 28132 kCGImageAlphaNoneSkipLast | 0 (default byte order) | kCGImagePixelFormatPacked
//获取宽高
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
<po sourceResolution (width = 7033, height = 10110)
;
//总像素大小
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
<po sourceTotalPixels 71103632
px;
//确定要应用于输入图像的比例比,从而得到定义大小的输出图像。
float imageScale = kDestTotalPixels / sourceTotalPixels;
<po imageScale 0.221207261
kDestTotalPixels(最大像素即60MB大小的 = 15728640px);
//使用图像比例尺计算output图像的宽度、高度
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
<po destResolution (width = 1555, height = 2236)
//这里和上面方法一样 创建output image的context
CGColorSpaceRef colorspaceRef = CGColorSpaceCreateDeviceRGB();
BOOL hasAlpha = ssCGImageRefContainsAlpha(sourceImageRef);
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (destContext == NULL) {
return image;
}
<po destContext <CGContext 0x600000e60e40> (kCGContextTypeBitmap) <<CGColorSpace 0x600001f667c0> (kCGColorSpaceDeviceRGB)> width = 1555, height = 2236, bpc = 8, bpp = 32, row bytes = 6240 kCGImageAlphaNoneSkipFirst | kCGImageByteOrder32Little
//context的插值质量设置为最高
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
//定义用于从输入图像到输出图像的增量blit的矩形的大小。
//由于iOS从磁盘检索图像数据的方式,我们使用的源图块宽度等于源图像的宽度。
//iOS必须以全宽“波段”从磁盘解码图像,即使当前图形上下文被剪切为该波段内的子图形。因此,我们通过将我们的图块大小设置为输入图像的整个宽度来充分利用由解码操作产生的所有像素数据。
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
//源图块高度是动态的。 由于我们以MB为单位指定了源图块的大小,因此请查看输入图像宽度可以设置多少像素的行。
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
<po sourceTile (origin = (x = 0, y = 0), size = (width = 7033, height = 745))
因为width * height=pixels所以反过来能计算出该块的高(宽是固定的高是动态的,为什么呢?上面的注释中解释了需要将图块(tile)设置为input image的整个width);
//输出图块与输入图块的比例相同,但缩放为图像比例。
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
<po destTile (origin = (x = 0, y = 5.2150177063252833E-310), size = (width = 1555, height = 164.79940950870514))
;destTile.height同样也是动态的;
//input image重叠与output image重叠的比例。 这是我们组装输出图像时每个图块重叠的像素数量。
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
<po sourceSeemOverlap 9
这个什么用暂时不理
CGImageRef sourceTileImageRef;
//计算生成output图像所需的读/写操作的数量。
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
//如果平铺的高度不能平均分割图像的高度,那么再增加一次迭代来计算剩余的像素。
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
<po iterations 14
将input image分14块区域扫描生成output bitmap
//添加图块重叠的部分,但保存原始图块高度以进行y坐标计算。
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
< po sourceTileHeightMinusOverlap 745
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
//创建对源图像的引用,并将其上下文剪切到参数rect。
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
//如果这是最后一个图块,则其大小可能小于源图块高度(不足一块)。
//调整dest tile大小以考虑该差异。
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
//从input图像到output图像读取和写入像素大小的像素部分。
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
在14次扫描中分别打印
sourceTile destTile
{{0, 9}, {7033, 754}} {{0, 2069.2005920410156}, {1555, 166.79940950870514}}
{{0, 754}, {7033, 754}} {{0, 1904.4011840820312}, {1555, 166.79940950870514}}
{{0, 1499}, {7033, 754}} {{0, 1739.6017761230469}, {1555, 166.79940950870514}}
{{0, 2244}, {7033, 754}} {{0, 1574.8023681640625}, {1555, 166.79940950870514}}
{{0, 2989}, {7033, 754}} {{0, 1410.0029296875}, {1555, 166.79940950870514}}
{{0, 3734}, {7033, 754}} {{0, 1245.2035522460938}, {1555, 166.79940950870514}}
{{0, 4479}, {7033, 754}} {{0, 1080.4041748046875}, {1555, 166.79940950870514}}
{{0, 5224}, {7033, 754}} {{0, 915.604736328125}, {1555, 166.79940950870514}}
{{0, 5969}, {7033, 754}} {{0, 750.8052978515625}, {1555, 166.79940950870514}}
{{0, 6714}, {7033, 754}} {{0, 586.005859375}, {1555, 166.79940950870514}}
{{0, 7459}, {7033, 754}} {{0, 421.20654296875}, {1555, 166.79940950870514}}
{{0, 8204}, {7033, 754}} {{0, 256.4071044921875}, {1555, 166.79940950870514}}
{{0, 8949}, {7033, 754}} {{0, 91.607666015625}, {1555, 166.79940950870514}}
{{0, 9694}, {7033, 754}} {{0, -73.191650390625}, {1555, 166.79940950870514}}
sourceTile的origin.y刚好是以sourceTileHeightMinusOverlap
递增,destTile的origin.y是以164.79940950870514即destTile.height
递减;说明从input image的最上面一个部分开始(从上到下)生成bitmap写入到output image最下面的区域里面(从下到上),最终生成一个完整的output bitmap context,为什么这样做呢?猜测是因为CoreGraphics框架坐标系统(左下为坐标原点)和UIKit坐标系统不一致(左上为坐标原点),在这里也没有使用CGContextTranslateCTM
转换一下;
//最后生成输出图片
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
这就是SDWebImage缩放解压缩大图的全部内容,这样做的好处就是分块压缩解码避免内存爆炸,其实这里也是参照苹果的做法,官方DemoLargeImageDownsizing;
incrementallyDecodedImageWithData
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
if (!_imageSource) {
_imageSource = CGImageSourceCreateIncremental(NULL);
}
UIImage *image;
// 更新数据,需要的是收到的所有数据而不是当前传入的data
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
if (_width + _height == 0) {//第一次收到数据
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);//获取该图片各种属性
if (properties) {
NSInteger orientationValue = 1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
//当我们绘制到Core Graphics时,我们会丢失方向信息,这意味着initWithCGIImage生成的图像有时会导致错误定向。 (与didCompleteWithError中的initWithData生成的图像不同。)因此将其保存在此处并稍后传递。
_orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];
#endif
}
}
if (_width + _height > 0) {
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
CGImageRelease(partialImageRef);
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
}
}
if (finished) {//最后完成的时候再释放_imageSource
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
return image;
}
因为网络传输的延迟,图片的数据不能一次性全部加载完,可能会一段一段地传过来,这种情况就需要渐进式地解码啦,ImageIO框架提供了专门用于渐进式解码的APICGImageSourceCreateIncremental(NULL)
,可以通过调用函数CGImageSourceUpdateDataProvider
或CGImageSourceUpdateData
向其添加数据。
encodedDataWithImage
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
#if SD_UIKIT || SD_WATCH
NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
[properties setValue:@(exifOrientation) forKey:(__bridge NSString *)kCGImagePropertyOrientation];
#endif
// Add your image to the destination.
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// Handle failure.
imageData = nil;
}
CFRelease(imageDestination);
return [imageData copy];
}
CGImageDestinationRef是ImageIO中负责写入数据的抽象出来的对象,可以表示单个图像或多个图像,可以包含缩略图图像以及每个图像的属性。使用CGImageDestinationCreateWithData
中imageData
就是要写入数据的对象,之后调用CGImageDestinationAddImage
写入数据和属性,最后使用CGImageDestinationFinalize
得到编码数据。
SDWebImageCodersManager
管理所有编码器的manager,也是编码器模块的唯一入口,这样做了一层中间层好处在于以后无论是修改还是添加删除只需要下层处理即可,上层无需做任何修改;可以使用自定义的编码器,默认使用SDWebImageImageIOCoder。
SDImageCache
SDImageCache的分为内存缓存和磁盘缓存,内存缓存任务主要由SDMemoryCache
负责。
内存缓存
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
@end
@interface SDMemoryCache <KeyType, ObjectType> ()
@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
@end
SDMemoryCache
基础于NSCache,所以自带以下特点
- 线程安全;
- key不需要实现NSCopying,因为对key是强引用并不是copy一份;
- 内存不足时自动释放对象;
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
weakCache对Key(URL)强引用对Value(Image)弱引用,这个weakCache有什么用呢,先看他的方法。
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
//如果启用弱内存缓存配置(默认启用)
if (key && obj) {
LOCK(self.weakCacheLock);
[self.weakCache setObject:obj forKey:key];//weakCache同时也持有一份Image(弱引用)
UNLOCK(self.weakCacheLock);
}
}
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];//查找weakCache
UNLOCK(self.weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = SDCacheCostForImage(obj);
}
[super setObject:obj forKey:key cost:cost];//再次同步到cache
}
}
return obj;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
//只清理Cache,但是保留weakCache
[super removeAllObjects];
}
- 在setObject的时候,如果启用了使用弱缓存机制,weakCache内部同时也缓存image,因为是弱引用所以不担心该image出现释放不掉内存泄露的问题;
- 在objectForKey时,如果NSCache查找不到该缓存(被系统释放掉了),则会在weakCache里面再查找该image,并再次恢复缓存,避免了要去磁盘查找或者重新去网络下载;
- 当收到MemoryWarning时只清理内存,并不会清理weakCache,因为是弱引用所以当有其他实例(例如UIImageView)持有image的时候并不会remove掉;
这种方式在某些情况下有有用,比如应用程序进入后台并清除内存时,再次进入前台时可能因为清理缓存导致的image显示闪烁问题(重新下载或者从磁盘读取),可以从弱缓存中同步回来。
4.这里缓存大小计算是以像素为单位的。
磁盘缓存
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
...内存缓存
if (toDisk) {
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
SDImageFormat format;
//通过判断图片是否有alpha通道来判断是png还是jpeg
if (SDCGImageRefContainsAlpha(image.CGImage)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
//将image编码
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
//磁盘储存的是image编码后的数据
[self _storeImageDataToDisk:data forKey:key];
}
}
...回调
}
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
...create directory
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//写入文件
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// skip iCloud备份
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
所有io操作都是在一个专门的串行队列里面完成,保证了线程的安全性和数据的准确性。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
//先检查内存缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {//只读取内存缓存
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
//利用operation cancel特性
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
//获取image编码数据
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
//解码
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {//这个options可以强制同步执行
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {//这个options可以强制同步执行
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
queryCacheOperationForKey
提供异步查询缓存功能,先查询内存缓存,如果缓存丢失则跑去磁盘缓存检索,加上从磁盘缓存检索出来的是image编码后的数据,需要被解码又可能还要压缩,所以去磁盘读取图片是非常耗时的操作,由于使用operation可以取消操作的特性,返回operation供上层使用者决定后续操作。
- (void)backgroundDeleteOldFiles {
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
[self deleteOldFilesWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
注册进入后台执行的方法,这里需要注意启动后台任务时要记得调用endBackgroundTask来标记停止任务,调用两次的原因是app可能在后台180s活动时间内完成也可能未完成,这里只会走到两者其一。
SDWebImageDownloaderOperation
- (void)start {
@synchronized (self) {//给当前operation加锁保证多线程的安全
if (self.isCancelled) {
self.finished = YES;
[self reset];//释放回调和session
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
//启用后台下载,如果在后台存活时间到期的时候operation还未执行完成则取消该操作
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
//unownedSession是由外部manager持有的所以是weak;这里避免unownedSession运行到一半的时候被外部释放需要捕捉一份
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
//delegateQueue为nil时将创建一个串行操作队列
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
//如果外部manager没有持有session,由operation创建个ownedSession
self.ownedSession = session;
}
//从NSURLCache获取缓存data
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
覆写start方法表明是个并行队列,主要做了以下几个工作:
- 因为多线程所以需要加锁保证线程安全;
- 当operation进入执行状态时,先检查isCancelled,如果被取消则重制Operation;
- 在dataTask开始前,如果APP启用后台下载且后台运行时间到期了,需要取消operation;
- 如果持有该operation的外部manager没有session,就在operation内部创建个临时session
- 设置
SDWebImageDownloaderIgnoreCachedResponse
标志位后会忽略NSURLCache的缓存,在这里获取NSURLCache缓存的response用来判断图片下载完成后是否是从NSURLCache缓存中获得的; - 创建NSURLSessionTask开始任务
- NSURLSessionDataDelegate
//receive response callback
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
self.expectedSize = expected;
self.response = response;
NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
BOOL valid = statusCode < 400;
//当服务器响应304并命中URLCache时,URLSession当前行为将返回状态码200。当返回304时却没有缓存数据时
if (statusCode == 304 && !self.cachedData) {
valid = NO;
}
if (valid) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
} else {
//状态码无效,并标记为已取消。不需要调用 [self.dataTask cancel] ',这可能会增加URLSession的生命周期
disposition = NSURLSessionResponseCancel;
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
});
if (completionHandler) {
completionHandler(disposition);
}
}
//receive data callback
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!self.imageData) {
//根据HTTP response中的expectedContentLength创建imageData
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data];//拼接数据
//渐进式显示图片
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
__block NSData *imageData = [self.imageData copy];
const NSInteger totalSize = imageData.length;
BOOL finished = (totalSize >= self.expectedSize);
if (!self.progressiveCoder) {
//为渐进解码创建一个新实例,以避免冲突
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
[((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
//就是之前的SDWebImageImageIO,每个operation持有一个,生命周期结束后释放
self.progressiveCoder = [[[coder class] alloc] init];
break;
}
}
}
//逐步解码编码器队列中的图像
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
//解码
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
//即使当finished=YES时,也不保留渐进式解码图像。因为它们是用于视图呈现的,而不是从downloader选项中获取完整的功能。而一些编码器的实现可能无法保持渐进式译码与常规译码的一致性。
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
}
//进度callback
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
//cache response callback
//所有触发条件都满足才触发
请求的是HTTP或HTTPS URL(或您自己的支持缓存的自定义网络协议)。
请求成功(状态码在200-299范围内)。
提供的响应来自服务器,而不是来自缓存。
session配置的缓存策略允许缓存。
提供的URLRequest对象的缓存策略(如果适用)允许缓存。
服务器响应中与缓存相关的头文件(如果存在)允许缓存。
响应大小足够小,可以合理地适应缓存。(例如,如果提供磁盘缓存,响应必须不大于磁盘缓存大小的5%)。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
NSCachedURLResponse *cachedResponse = proposedResponse;
//默认情况下,request阻止使用NSURLCache。使用此标志时,NSURLCache将与默认策略一起使用。
if (!(self.options & SDWebImageDownloaderUseNSURLCache)) {
// 防止缓存响应
cachedResponse = nil;
}
if (completionHandler) {
completionHandler(cachedResponse);
}
}
//complete callback
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
... 发送通知
if (error) {
[self callCompletionBlocksWithError:error];
[self done];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
__block NSData *imageData = [self.imageData copy];
if (imageData) {
//如果指定仅通过“SDWebImageDownloaderIgnoreCachedResponse”忽略NSURLCache而使用SDWebImageCache缓存的数据,那么我们应该检查缓存的数据是否等于图像数据
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
//cachedData=imageData时表示该imageData是从NSURLCache缓存中获取的
//设置SDWebImageDownloaderIgnoreCachedResponse标志后当获取到缓存callback中image和imageData都设置为nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
[self done];
} else {
// 解码编码器队列中的图像
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
BOOL shouldDecode = YES;
//不强制解码gif和WebPs
if (image.images) {
shouldDecode = NO;
} else {
#ifdef SD_WEBP
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
if (imageFormat == SDImageFormatWebP) {
shouldDecode = NO;
}
#endif
}
//下面都是解码和回调了
if (shouldDecode) {
if (self.shouldDecompressImages) {
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}
});
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
[self done];
}
} else {
[self done];
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
/*
NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
NSURLSessionAuthChallengeUseCredential:使用指定的证书
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消权限认证
*/
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// 从服务器返回的受保护空间(就是证书)中拿到证书的类型
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {//判断服务器返回的证书是否是服务器信任的
if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
} else {
//创建证书
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
} else {
if (challenge.previousFailureCount == 0) {
if (self.credential) {
credential = self.credential;
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
//安装证书
if (completionHandler) {
completionHandler(disposition, credential);
}
}
SDWebImageDownloaderOperation
主要的工作就是处理Operation具体的下载任务,收到downloader分发的request之后创建下载任务到收到图片数据后的处理,在下载图片数据完成后马上在子线程对原始图片数据解码然后通过生成bitmap创建image。
SDWebImageDownloader
SDWebImageDownloader
的工作就是管理Operation
,接收到上层URL后开启一个下载任务,待任务完成后再转交image给上层。
- (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options {
NSTimeInterval timeoutInterval = self.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
//为了防止潜在的重复缓存(NSURLCache + SDImageCache),禁用了缓存的图像请求
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
//默认运行处理cookies
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (self.headersFilter) {
request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [self allHTTPHeaderFields];
}
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
operation.shouldDecompressImages = self.shouldDecompressImages;
if (self.urlCredential) {
operation.credential = self.urlCredential;
} else if (self.username && self.password) {
operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[self.lastAddedOperation addDependency:operation];
self.lastAddedOperation = operation;
}
return operation;
}
这个方法主要是根据url创建request并设置request的一些基础配置和创建operation和传递外部的配置,这里值得一提的就是HTTPShouldUsePipelining
,了解这个之前首先要了解HTTP长连接和HTTP Pipelining
HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。
使用了HTTP长连接(HTTP persistent connection )之后的好处,包括可以使用HTTP 流水线技术(HTTP pipelining,也有翻译为管道化连接),它是指,在一个TCP连接内,多个HTTP请求可以并行,下一个HTTP请求在上一个HTTP请求的应答完成之前就发起。从 wiki上了解到这个技术目前并没有广泛使用,使用这个技术必须要求客户端和服务器端都能支持,目前有部分浏览器完全支持,而服务端的支持仅需要:按 HTTP请求顺序正确返回Response(也就是请求&响应采用FIFO模式),wiki里也特地指出,只要服务器能够正确处理使用HTTP pipelinning的客户端请求,那么服务器就算是支持了HTTP pipelining。
摘自 HTTP的长连接和短连接
开启HTTPShouldUsePipelining
后,在上个Request收到Response之前就能继续发送Request,好处是可以提高网络请求的效率,坏处则是可能需要服务器的支持(需要保证HTTP按正确正确的顺序返回Response);而苹果默认是关闭的,官方文档中表明就算设置为YES也不保证HTTP Pipelining行为(比如POST请求是无效的,HTTP<1.1不work,HTTP>=1.1&&<2中服务器不支持也会不work,HTTP2采用的是二进制流传输数据且是多路复用所以也不需要管道流水技术),而AFNetWorking曾经在GET和HEAD请求中默认开启过,但是后来因为这个issue关闭了,综上来看似乎这种优化并不会有带来很大的收益甚至用不好还可能出问题,但是在SDWebImage中一直启用貌似也没有人反馈过问题,可能原因是SDWebImage默认支持最大并发数为6并不会形成此类问题。
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
//URL将用作回调字典的键,所以它不能是nil。如果是nil,立即调用没有图像或数据的callback。
LOCK(self.operationsLock);
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished) {
//创建operation
operation = [self createDownloaderOperationWithUrl:url options:options];
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
//operation持有completeBlock 所以当下载完成后由各自的operation唤起block
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
这里的downloadOperationCancelToken
实际上就是储存两个block的字典,当调用cancel时会把两个block释放,当该operation不再持有任何block时就认为需要取消该操作。
SDWebImageManager
SDWebImageManager
的主要工作就是将异步下载器(SDWebImageDownloader)与图像缓存存储(SDImageCache)绑定在一起。
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
1.SDWebImageCombinedOperation
继承于SDWebImageOperation
,并且持有downloader下载任务传过来的 SDWebImageDownloadToken
,提供一个对象让上层调用者持有这个下载任务实例还有cancel方法取消下载操作。
2.通过缓存模块在缓存中查找缓存数据
3.根据查找缓存结果和设置option决定是否下载或回调
UIImageView+WebCache
注意这里每个UIImageView同时只能关联一个operation,如果有新的下载任务开始,要先取消之前的下载任务。
总结
stay hungry, stay foolish
参考和一些延伸阅读
iOS平台图片编解码入门教程(Image/IO篇)
what is cginterpolationquality
谈谈 iOS 中图片的解压缩
ios核心动画高级技巧
iOS性能优化——图片加载和处理
绘制像素到屏幕上
ios如何避免图像解压缩的时间开销
SDWebImage-解码、压缩图像
Image Resizing Techniques
avoiding image decompression sickness
iOS 处理图片的一些小 Tip
CFStringTransform
NSURLCache
Safari on iOS 5 Randomly Switches Images
iOS 保持界面流畅的技巧
iOS 视图、动画渲染机制探究