https://github.com/rs/SDWebImage
一个异步图片下载及缓存的库。
只要有图片的 url,就能下载到图片,使用 SDWebImage 的好处就是缓存机制,每次取图片先判断是否在内存中,如果没有,再到缓存中查找,找到了就直接加载,在缓存中找不到才重新下载,url 也会记录,记录是否是失效的 url,是则不会再尝试。
下载到的图片会进行缓存,用于下次可以直接加载。图片下载、解码、转换都是异步进行,不会阻塞主线程。
// 设置缓存的类型、方式、路径等
SDImageCache
// 兼容类(Compat),定义了很多宏和一个转换图片的方法
SDWebImageCompat
// 解码器(Decoder),让图片色彩转换(涉及 color space)
SDWebImageDecoder
// 下载器(Download),设置下载相关,要用到 SDWebImageDownloaderOperation
SDWebImageDownloader
// 下载器的操作(Operation)
SDWebImageDownloaderOperation
// 管理(Manager)图片下载、取消操作,判断 url 是否已缓存等
SDWebImageManager
// 图片操作(Operation),很多类都需要用到
SDWebImageOperation
// 预抓取器(Prefetcher),预先下载 url 中的图片
SDWebImagePrefetcher
// 按钮图片的缓存
UIButton+WebCache
// 缓存 GIF
UIImage+GIF
// 判断图片类型,png/jpeg/gif/webp
NSData+ImageContentType
// 缓存多种格式的图片,要用到 NSData+ImageContentType 的判断图片类型方法和 UIImage+GIF 的判断是否为 gif 图的方法,以及 ImageIO 里面的方法
UIImage+MultiFormat
// 缓存高亮图片
UIImageView+HighlightedWebCache
// 加载及缓存 UIImageView 的图片
UIImageView+WebCache
// 缓存的操作(Operation),缓存,取消操作,移除缓存
UIView+WebCacheOperation
图片操作,只有头文件,定义了协议 SDWebImageOperation,里面也只有取消方法。
这个类后面很多类都要用到。
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
这个文件是 NSData 的分类,只有一个方法,传入图片数据,根据图片的头标识来确定图片的类型。头标识都不一样,只需获取文件头字节,对比十六进制信息,判断即可。
图片文件 | 头标识 | 十六进制头字节 |
---|---|---|
jpeg/jpg | FFD8 | 0xFF |
png | 8950 | 0x89 |
gif | 4749 | 0x47 |
tiff | 4D4D / 4949 | 0x49/0x4D |
最新(2020.2.23)现已支持多种图片格式
static const SDImageFormat SDImageFormatUndefined = -1;
static const SDImageFormat SDImageFormatJPEG = 0;
static const SDImageFormat SDImageFormatPNG = 1;
static const SDImageFormat SDImageFormatGIF = 2;
static const SDImageFormat SDImageFormatTIFF = 3;
static const SDImageFormat SDImageFormatWebP = 4;
static const SDImageFormat SDImageFormatHEIC = 5;
static const SDImageFormat SDImageFormatHEIF = 6;
Webp 格式开头是 0x52,但是还有可能是其他类型文件,所以要识别
符合这些条件的才是 webp 图片文件。
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
// File signatures table: http://www.garykessler.net/library/file_sigs.html
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
case 0x52: {
if (data.length >= 12) {
//RIFF....WEBP
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
break;
}
case 0x00: {
if (data.length >= 12) {
//....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
if ([testString isEqualToString:@"ftypheic"]
|| [testString isEqualToString:@"ftypheix"]
|| [testString isEqualToString:@"ftyphevc"]
|| [testString isEqualToString:@"ftyphevx"]) {
return SDImageFormatHEIC;
}
//....ftypmif1 ....ftypmsf1
if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
return SDImageFormatHEIF;
}
}
break;
}
}
return SDImageFormatUndefined;
}
最基础的配置文件,为了兼容苹果各个平台。
兼容类,这个类定义了很多宏还有一个伸缩图片的方法。
这个方法定义成 C 语言式的内联方法。
核心代码如下,传入 key 和图片,如果 key 中出现 @2x 就设定 scale 为2.0,出现 @3x 就设定 scale 为3.0,然后伸缩图片.
CGFloat scale = [UIScreen mainScreen].scale;
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
这个是解码器类,只定义了一个解码方法,传入图片,返回的也是图片。
不解码也是可以使用的,假如说我们通过 imageNamed: 来加载 image,系统默认会在主线程立即进行图片的解码工作。这一过程就是把 image 解码成可供控件直接使用的位图。
当在主线程调用了大量的 imageNamed: 方法后,就会产生卡顿了。为了解决这个问题我们有两种比较简单的处理方法:
我们不使用 imageNamed:加载图片,使用其他的方法,比如 imageWithContentsOfFile:
从网络上下载回来的图片也不能直接在 UI 控件上显示,所以 SDWebImage 选择自己解码图片,而且 SDWebImage 将解码操作基本都放在了子线程来执行。
CGImageRef 是一个指针类型。
typedef struct CGImage * CGImageRef;
获取传入图片的 alpha 信息,然后判断是否符合苹果定义的 CGImageAlphaInfo,如果是就返回原图片。
static const size_t kBytesPerPixel = 4; // 每个像素占用四个字节
static const size_t kBitsPerComponent = 8;
static const CGFloat kDestImageSizeMB = 60.0f; // 大图片 scaleDown 后制定的最大开销
static const CGFloat kSourceImageTileSizeMB = 20.0f; // 图片分块后每个块大小
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; // 1MB 包含多少个字节
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; // 1MB 大小能包含多少像素
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; // 图片 scaleDown 后的最大像素数
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; // 图片 scaleDown 过程中每个块的像素数量
这个方法主要是单纯的解码图片,方便 GPU 直接渲染。
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}
// 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 imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerRow = kBytesPerPixel * 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.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
将图片解码并进行 scaleDown 的方法:
主要处理流程:
+ (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;
// Allocate enough pixel data to hold the output image.
void* destBitmapData = malloc( bytesPerRow * destResolution.height );
if (destBitmapData == NULL) {
return image;
}
// 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(destBitmapData,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
free(destBitmapData);
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;
}
}
是否需要将图片解码:
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
if (image.images != nil) {
return NO;
}
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// do not decode images with alpha
if (anyAlpha) {
return NO;
}
return YES;
}
是否需要将图片进行 scaleDown,如果原图片总的像素数的大小 > kDestTotalPixels 就需要 scaleDown
+ (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;
}
获取图片的颜色空间,什么是图片的颜色空间?
获取图片的宽高和 color space(指定颜色值如何解释),判断 color space 是否支持,不支持就转换为支持的模式(RGB),再用图形上下文根据获得的信息画出来,释放掉创建的 CG 指针再返回图片。
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace) {
colorspaceRef = CGColorSpaceCreateDeviceRGB();
CFAutorelease(colorspaceRef);
}
return colorspaceRef;
}
缓存操作的 UIView 的分类,支持三种操作,也是整个库中比较核心的操作。
但是首先我们来了解三种操作都要用到的存储数据的方法。
这两个方法用的是 OC 中 runtime 方法,原理是两个文件关联方法,和上层的存储方法差不多,传入 value 和 key 对应,取出也是根据 key 取出 value ,object 传入 self 即可。
// 传入 object、key、value,policy
// policy 即存储方式,和声明使用几种属性大致相同,有 copy,retain,copy,retain_nonatomic,assign 五种)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 传入 object 和 key 返回 value
id objc_getAssociatedObject(id object, const void *key)
这个方法是三种操作都要用到的,获得数据。
这个方法是使用前面两个方法,根据缓存加载数据。
有缓存则从缓存中取出数据,没有则缓存数据,返回格式是字典格式。
- (SDOperationsDictionary *)sd_operationDictionary {
@synchronized(self) {
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
接下来是三种操作
从获得数据方法获得数据,传入 key,先调用第二个方法停止操作,再根据 key 缓存数据。
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
if (key) {
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
@synchronized (self) {
[operationDictionary setObject:operation forKey:key];
}
}
}
}
先获得方法一的返回字典数据,传入 key 在返回的字典中查找是否已经存在,如果存在则取消所有操作
conformsToProtocol 方法如果符合这个协议(协议中声明了取消方法),调用协议中的取消方法。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
获得方法一的数据,传入 key,如果 key 对应的数据在缓存中则移除。
- (void)sd_removeImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
下载器类,需要用到 SDWebImageDownloaderOperation 类,下载器操作,后面会说到。
// 下载队列的最大下载数
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
// 当前下载数
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
// 下载超时的时间
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
// 是否解压图片,默认是
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 下载器顺序,枚举类型,有两种,先进先出,还是后进先出
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
//***************** 还有一些用户属性
// url 证书
@property (strong, nonatomic) NSURLCredential *urlCredential;
// 用户名
@property (strong, nonatomic) NSString *username;
// 密码
@property (strong, nonatomic) NSString *password;
// 头像过滤器,block 指针类型,接受 url 和字典 headers
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
- (nonnull instancetype)init {
return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}
- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
self = [super init];
if (self) {
if (!config) {
config = SDWebImageDownloaderConfig.defaultDownloaderConfig;
}
_config = [config copy];
[_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
NSString *userAgent = nil;
#if SD_UIKIT
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#elif SD_WATCH
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
#elif SD_MAC
userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
#endif
if (userAgent) {
if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSMutableString *mutableUserAgent = [userAgent mutableCopy];
if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
userAgent = mutableUserAgent;
}
}
headerDictionary[@"User-Agent"] = userAgent;
}
headerDictionary[@"Accept"] = @"image/*,*/*;q=0.8";
_HTTPHeaders = headerDictionary;
_HTTPHeadersLock = dispatch_semaphore_create(1);
_operationsLock = dispatch_semaphore_create(1);
NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
if (!sessionConfiguration) {
sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
_session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
传入 url,下载器选项(接下来会说),进度 block,完成回调 block
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
下载器的操作
直接看前面下载器需要用到的初始化方法。
需要初始化了各种属性,进度,完成,取消等的回调 Block 数组。
_callbackBlocks = [NSMutableArray new];
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {
if ((self = [super init])) {
_request = [request copy];
_options = options;
_context = [context copy];
_callbackBlocks = [NSMutableArray new];
_responseModifier = context[SDWebImageContextDownloadResponseModifier];
_decryptor = context[SDWebImageContextDownloadDecryptor];
_executing = NO;
_finished = NO;
_expectedSize = 0;
_unownedSession = session;
_coderQueue = dispatch_queue_create("com.demo.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
#if SD_UIKIT
_backgroundTaskId = UIBackgroundTaskInvalid;
#endif
}
return self;
}
图片管理器,负责图片的下载,转换,缓存等。
这里先说明 SDWebImageOptions
1 << X 这种是位运算符,1左移多少位,后面要用到,说明一下。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
* 默认情况下,当一个URL下载失败时,该URL会被列入黑名单,这样库就不会继续尝试。
* 此标志禁用此黑名单。
*/
SDWebImageRetryFailed=1<<0,
/**
* 默认情况下,图像下载在 UI 交互期间启动,此标志禁用此功能,
* 例如,导致 UIScrollView 下载延迟减速。
*/
SDWebImageLowPriority=1<<1,
/**
* 此标志启用渐进式下载,图像在下载过程中会像浏览器那样渐进式显示。
* 默认情况下,图像仅在完全下载后显示。
*/
SDWebImageProgressiveLoad=1<<2,
/**
* 即使映像已缓存,也要尊重 HTTP 响应缓存控件,并在需要时从远程位置刷新映像。
* 磁盘缓存将由 NSURLCache 而不是 SDWebImage 处理,这会导致性能略有下降。
* 此选项有助于处理同一请求 URL 后面的图像更改,例如 Facebook graph api profile pics。
* 如果刷新了缓存的图像,则使用缓存的图像调用一次完成块,然后使用最终图像再次调用完成块。
* 仅当不能使用嵌入的缓存破坏参数使 UR L保持静态时,才使用此标志。
*/
SDWebImageRefreshCached=1<<3,
/**
*在 iOS4+ 中,如果应用程序转到后台,请继续下载图像。这是通过请求系统额外的后台时间让请求完成。如果后台任务过期,则操作将被取消。
*/
SDWebImageContinueInBackground=1<<4,
/**
* 通过设置处理存储在 NSHTTPCookieStore 中的 cookie
* NSMutableURLRequest.HTTPShouldHandleCookies = YES;
*/
SDWebImageHandleCookies=1<<5,
/**
* 启用以允许不受信任的SSL证书。
* 用于测试目的。在生产中小心使用。
*/
SDWebImageAllowInvalidSLCertificates=1<<6,
/**
* 默认情况下,按图像排队的顺序加载图像。这面旗子把他们移到排在队伍前面。
*/
SDWebImageHighPriority=1<<7,
/**
* 默认情况下,在加载图像时加载占位符图像。这个标志会延迟装货
* 直到图像加载完毕。
*/
SDWebImageDelayPlaceholder=1<<8,
/**
* 我们通常不会对动画图像应用变换,因为大多数变换器无法管理动画图像。
* 无论如何,使用此标志来转换它们。
*/
SDWebImageTransformAnimatedImage=1<<9,
/**
* 默认情况下,图像在下载后添加到 imageView。但在某些情况下,我们想在设置图像之前使用(例如应用过滤器或添加交叉淡入动画)
* 如果要在成功时手动设置完成中的图像,请使用此标志
*/
SDWebImageAvoidAutosetImage1<<10,
/**
* 默认情况下,图像根据其原始大小进行解码。
* 此标志将缩小图像的大小,使其与设备的受限内存兼容。
* 要控制限制内存字节,请选中“SDImageCoderHelper.defaultScaleDownLimitBytes”(在iOS上默认为60MB)
* 这实际上将转换为使用5.5.0版的上下文选项“imageThumbnailPixelSize”(在iOS上默认为(39663966))。以前没有。
* 此标志也会影响v5.5.0中的渐进图像和动画图像。以前没有。
* @note如果需要细节控件,最好使用上下文选项“imageThumbnailPixelSize”和“imagePreserveSpectratio”。
*/
SDWebImageScaleDownLargeImages=1<<11,
/**
* 默认情况下,当图像已经缓存在内存中时,我们不会查询图像数据。此掩码可以强制同时查询图像数据。但是,此查询是异步的,除非指定'SDWebImageQueryMemoryDataSync'`
*/
SDWebImageQueryMemoryData=1<<12,
/**
* 默认情况下,当您只指定“SDWebImageQueryMemoryData”时,我们将异步查询内存映像数据。同时结合这个掩码来同步查询内存图像数据。
* @note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元格重用期间闪烁。
*/
SDWebImageQueryMemoryDataSync=1<<13,
/**
* 默认情况下,当内存缓存未命中时,我们异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
* @注意这 3 个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参见wiki页面。
* @note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元格重用期间闪烁。
*/
SDWebImageQueryDiskDataSync=1<<14,
/**
* 默认情况下,当缓存丢失时,将从加载程序加载图像。此标志只能阻止从缓存加载。
*/
SDWebImageFromCacheOnly=1<<15,
/**
* 默认情况下,我们在从加载程序加载图像之前查询缓存。此标志只能阻止从加载程序加载。
*/
SDWebImageFromLoaderOnly=1<<16,
/**
* 默认情况下,在图像加载完成后使用“SDWebImageTransition”进行某些视图转换时,此转换仅适用于从网络下载图像。此掩码还可以强制对内存和磁盘缓存应用视图转换。
*/
SDWebImageForceTransition=1<<17,
/**
* 默认情况下,我们将在缓存查询和从网络下载期间解码背景图像。这有助于提高性能,因为在屏幕上渲染图像时,需要首先对其进行解码。但这发生在主队列上的核心动画。
* 然而,这个过程也可能增加内存使用。如果由于内存消耗过多而遇到问题,则此标志可以阻止对图像进行解码。
*/
SDWebImageAvoidDecodeImage=1<<18,
/**
* 默认情况下,我们解码动画图像。此标志只能强制解码第一帧并生成静态图像。
*/
SDWebImageDecodeFirstFrameOnly=1<<19,
/**
* 默认情况下,对于“SDAnimatedImage”,我们在渲染期间解码动画图像帧以减少内存使用。但是,当动画图像被许多imageview共享时,可以指定将所有帧预加载到内存中以减少CPU使用。
* 这实际上会触发后台队列中的“preloadAllAnimatedImageFrames”(仅限磁盘缓存和下载)。
*/
SDWebImagePreloadAllFrames=1<<20,
/**
* 默认情况下,当您使用“SDWebImageContextAnimatedImageClass”上下文选项(如使用“SDAnimatedImageView”(设计为使用“SDAnimatedImage”)时,当内存缓存命中或图像解码器不可用时,我们仍可以使用“UIImage”生成与您的自定义类完全匹配的类作为回退解决方案。
* 使用此选项,可以确保我们始终使用您提供的类回调映像。如果生成失败,将使用代码为“SDWebImageErrorBadImageData”的错误。
* 注意:此选项与“SDWebImageDecodeFirstFrameOnly”不兼容,后者总是生成UIImage/NSImage。
*/
SDWebImageMatchAnimatedImageClass=1<<21,
/**
* 默认情况下,从网络加载图像时,图像将写入缓存(内存和磁盘,由“storeCacheType”上下文选项控制)
* 这可能是一个异步操作,最终的“SDInternalCompletionBlock”回调不能保证写入的磁盘缓存已完成,并可能导致逻辑错误。(例如,在完成块中修改磁盘数据,但是磁盘缓存尚未就绪)
* 如果需要处理完成块中的磁盘缓存,则应使用此选项确保在回调时已写入磁盘缓存。
* 注意,如果在使用自定义缓存序列化程序或使用转换器时使用此选项,我们也将等待输出图像数据写入完成。
*/
SDWebImageWaitStoreCache=1<<22,
};
这里包含了各种选择
传入 url,上面的 options,进度 block,完成回调 block。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
预抓取器,用来预抓取图片
// 预抓取图片
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
// 取消预抓取图片
- (void)cancelPrefetching;
先来看预抓取图片
传入 url,进度 block,完成回调 block
首先取消抓取,然后重新开始
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
if (!urls || urls.count == 0) {
if (completionBlock) {
completionBlock(0, 0);
}
return nil;
}
SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new];
token.prefetcher = self;
token.urls = urls;
token->_skippedCount = 0;
token->_finishedCount = 0;
token->_totalCount = token.urls.count;
atomic_flag_clear(&(token->_isAllFinished));
token.loadOperations = [NSPointerArray weakObjectsPointerArray];
token.prefetchOperations = [NSPointerArray weakObjectsPointerArray];
token.progressBlock = progressBlock;
token.completionBlock = completionBlock;
[self addRunningToken:token];
[self startPrefetchWithToken:token];
return token;
}
最后调用 startPrefetchingAtIndex: 方法,再调用 self.manager 的核心方法,即开始下载图片
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
很多加载方法最终都会以缺省参数方式或者直接调用这个方法,传入一个 URL,一个用来初始化的 image,一个 options(枚举,下面详细说明),一个 progressBlock(返回图片接受进度等),一个completedBlock(完成回调 block)
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock;
首先根据 url 缓存图片,这里用到的是 OC 的 runtime中的关联方法(见4)
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
然后判断 options(见7)是其他选择则直接给图片赋值 placehoder 图片,这里判断使用的是 & 与 位运算符,SDWebImageDelayPlacehoder 是 1 << 9,1左移9位与 options 相与。
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
如果 url 存在,则定义图片操作,使用图片管理器的单例来调用核心方法(下载图片方法)
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 过程省略
}
gif 的实现使用了 ImageIO 中的 CGImageSourceRef
用获得的 gif 数据得到 CGImageSourceRef,然后算出时间,在这个时间内把图片一帧一帧的放进一个数组,最后再把这个数组和时间转成图片,就成了 gif
+ (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data {
if (!data) {
return nil;
}
return [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0];
}
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
if (!data) {
return nil;
}
CGFloat scale = 1;
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1) ;
}
CGSize thumbnailSize = CGSizeZero;
NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
BOOL preserveAspectRatio = YES;
NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
if (preserveAspectRatioValue != nil) {
preserveAspectRatio = preserveAspectRatioValue.boolValue;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!source) {
return nil;
}
UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize];
CFRelease(source);
if (!image) {
return nil;
}
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
return image;
}