SDWebImage图片的缓存的处理大部分都是在 SDImageCache 这个类中实现,SDWebImage 的图片缓存采用的是 Memory 和 Disk 双重缓存机制。
/**
* SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed
* asynchronous so it doesn’t add unnecessary latency to the UI.
*/
//SDImageCache维护一个内存缓存和一个可选的磁盘缓存。执行磁盘缓存写入操作是
异步的,所以它不会给UI增加不必要的延迟
@interface SDImageCache : NSObject
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
@end
@interface SDMemoryCache : NSCache
@end
内存缓存
内存缓存是使用SDMemoryCache(继承自系统的NSCache)实现的,它是一个类似于 NSDictionary 的集合类,用于在内存中存储我们要缓存的数据。磁盘缓存
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
[self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
将图片存放到沙盒的NSCachesDirectory 目录中
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
为每一个缓存文件生成一个 md5 文件名, 存放到文件中
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
// File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
ext = nil;
}
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
return filename;
}
缓存清理
SDWebImage 会在每次 APP 结束和进入后台的时候执行清理任务。 清理缓存分两步进行。
- 先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够
- 按文件时间从早到晚排序,先清除最早的缓存文件,直到剩余空间达到要求
//初始化时添加观察者
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
//只有在 maxCacheSize 大于 0 并且当前缓存空间大于 maxCacheSize 的时候才进行第二步的缓存清理
}
SDWebImage 并没有对 maxCacheSize 设置默认值。SDWebImage 在默认情况下不会对缓存空间设限制。
缺点:在设置-通用-储存空间中用户可以查看每个 APP 的空间使用情况, 如果你的 APP 占用空间比较大的话,就很容易成为用户的卸载目标
过滤URL,禁用缓存
如果想过滤特定URL,不使用缓存机制,可以在对应位置加入过滤代码
SDWebImageManager.sharedManager.cacheKeyFilter = ^NSString * _Nullable(NSURL * _Nullable url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
if ([[url absoluteString] isEqualToString:@"要过滤的URL"]) {
return nil;
}
return [url absoluteString];
};
//取缓存
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
清除特定图片缓存
SDWebImage加载图片优先会从缓存中取,而不是每次重新请求加载,那么如果我们的头像/广告图需要实时刷新,应该怎么解决?
- 使用 options:SDWebImageRefreshCached 刷新缓存
该方法可能会有闪烁,甚至有时并没有更新图片的问题存在 - 每次清除掉图片缓存,重新加载的方式
NSURL *imageURL = [NSURL URLWithString:@"实时更新的URL"];
// 获取对应URL链接的key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:imageURL];
NSString *pathStr = [[SDImageCache sharedImageCache] defaultCachePathForKey:key];
// 删除对应key的文件
[[SDImageCache sharedImageCache] removeImageForKey:key withCompletion:^{
[self.tempImageView sd_setImageWithURL:imageURL placeholderImage:[UIImage imageNamed:@"placeholderHead.png"]];
}];
SDWebImage工作流程
SDWebImageManager同时管理SDImageCache和SDWebImageDownloader两个类
讲解SDWebImageManager是如何下载图片之前,我们先看一下这个类的几个重要的属性:
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;//管理缓存
@property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader //下载器*imageDownloader;
@property (strong, nonatomic, nonnull) NSMutableSet *failedURLs;//记录失效url的名单
@property (strong, nonatomic, nonnull) NSMutableArray *runningOperations;//记录当前正在执行的操作
SDWebImageOptions
SDWebImageScaleDownLargeImages
默认情况下,图像将根据其原始大小进行解码。 在iOS上,此标志会将图片缩小到与设备的受限内存兼容的大小。如果设置了“SDWebImageProgressiveDownload”标志,则会关闭缩小比例
SDWebImage-解码、压缩图像
参考1
参考2
- SDWebImage最外层的类是我们常用的UIImageView +WebCache类,下面是这个类的公共接口
// ============== UIImageView + WebCache.h ============== //
- (void)sd_setImageWithURL:(nullable NSURL *)url;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(nullable NSURL *)url
completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
//使用方法
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcode.jpg"]];
- 最后都会调用 UIView (WebCache)分类 的方法
为什么不是UIImageView+WebCache而要上一层到UIView的分类里呢?
因为SDWebImage框架也支持UIButton的下载图片等方法,所以需要在它们的父类:UIView里面统一一个下载方法。
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary *)context {
//UIView+WebCacheOperation 的 operationDictionary
//下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
//这里作者专门创造一个分类UIView+WebCacheOperation来管理操作缓存(字典)
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
//主线程设置展位图片
if (!(options & SDWebImageDelayPlaceholder)) {
if (group) {
dispatch_group_enter(group);
}
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
//内部会调用 SDWebImageManager 的 loadImageWithURL 方法来处理这个图片 URL
id operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { }
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
UIView+WebCacheOperation分类来关联对象,管理操作缓存
// ============== UIView+WebCacheOperation.m ============== //
- (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;
}
}
- (void)sd_setImageLoadOperation:(nullable id)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];
}
}
}
}
- SDWebImageManager 内部的 downloadImageWithURL 方法会调用 SDImageCache 类的 queryCacheOperationForKey 方法,查询图片缓存
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { }
//SDImageCache 内部的缓存查询策略
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
//缓存没有,再查询磁盘
@autoreleasepool {
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);
//如果 Disk Cache 查询成功,还会把得到的图片再次设置到 Memory Cache 中。 这样做可以提高查询图片的效率
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
}
- 如果缓存查询成功, 会直接返回缓存数据。 如果不成功,再开始使用 SDWebImageDownloader 请求网络下载图片
if (url) {
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
[self.runningOperations addObject:operation];
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {}
//在操作缓存字典(operationDictionary)里添加operation,表示当前的操作正在进行
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
//如果url不存在,就在completedBlock里传入error(url为空)
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
4.1. 如果下载失败, 会把失败的图片地址写入 failedURLs 集合
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
因为 SDWebImage 默认会有一个对上次加载失败的图片拒绝再次加载的机制。
failedURLs这个属性是在内存中存储的,如果图片加载失败, SDWebImage 会在本次 APP 会话中都不再重试这张图片了。当然这个加载失败是有条件的,如果是超时失败(15s),不会写入 failedURLs 集合
SDWebImage 这样做可能是为了避免不必要的资源浪费,提高性能吧。
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//如果options的值没有设置为失败后重试并且url下载失败过,执行完成block返回错误
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
如果在加载图片的时候设置 SDWebImageRetryFailed 标记,这样 SDWebImage 就会加载failedURLs集合中的图片。
4.2. 如果下载图片成功了,接下来就会使用 [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil] 方法将它写入缓存和磁盘,并且调用 completedBlock 回调显示图片
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
dispatch_main_async_safe(^{
if (operation && !operation.isCancelled && completionBlock) {
completionBlock(image, data, error, cacheType, finished, url);
}
});
//如果已经完成下载了,将operation移除
[self safelyRemoveOperationFromRunning:operation];
//[self.runningOperations removeObject:operation]; 就是这句代码
以上就是SDWebImage 的整体图片加载流程
SDWebImage的下载队列机制
- 图片下载是通过SDWebImageDownloader和SDWebImageDownloaderOperation类来完成的。
- SDWebImageDownloaderOperation封装了单个图片下载操作,继承自NSOperation,这个类主要实现了图片下载的具体操作、及图片下载完成后的图片解压缩、Operation生命周期管理等
- SDWebImageDownloader是用来管理SDWebImageDownloaderOperation图片下载任务的,内部维护着一个私有并发下载队列downloadQueue,默认最大并发数是6,SDWebImage的下载队列默认情况下是SDWebImageDownloaderFIFOExecutionOrder,是先进先出的
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
_downloadTimeout = 15.0;
}
return self;
}
//核心方法:下载图片
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
NSOperation *operation = [self.URLOperations objectForKey:url];
[self.downloadQueue addOperation:operation];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
return token;
}
项目使用注意事项
- 下载图片完成的回调不能为空,
[[SDWebImageManager sharedManager] loadImageWithURL:url
options:SDWebImageRetryFailed|SDWebImageContinueInBackground
progress:nil
completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
}];
- (id )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
}
- 我们可以单独使用 SDWebImageDownloader 来下载图片,但是图片内容不会缓存,因为缓存的逻辑是在SDWebImageManager中处理的
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// do something with image
}
}];
gif的加载
4.0版本之后,SDWebImage依赖 FLAnimatedImage进行了gif的加载,需要我们单独导入pod 'SDWebImage/GIF',并且需要使用FLAnimatedImageView 代替 UIImageView,如果继续使用UIImageView来显示GIF图,将只会显示第一帧。
gif的加载主要是FLAnimatedImage 、FLAnimatedImageView 、FLAnimatedImageView+WebCache这3个类来处理GIF图片;动态图片的数据通过 ALAnimatedImage对象来封装。FLAnimatedImageView是UIImageView的子类。
SDWebImage4. x之后UIImageView默认 不启用完整GIF解码/编码。因为它会降低FLAnimatedImageView的性能。不过您可以启用UIImageView实例通过添加内置的GIF编码器
[[SDWebImageCodersManager sharedInstance] addCoder:[SDWebImageGIFCoder sharedCoder]];
@interface FLAnimatedImageView : UIImageView
@property (nonatomic, strong) FLAnimatedImage *animatedImage;// 动态图片的封装对象
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
@property (nonatomic, strong, readonly) UIImage *currentFrame;//当前动画帧对应的UIImage对象
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;//当前图片镇对应的索引
//指定动态图片执行所在的runloop的mode。NSRunLoopCommonMode
@property (nonatomic, copy) NSString *runLoopMode;
@end
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:^(UIImage *image, NSData *imageData) {
//根据NSData的类型获取图片的类型
BOOL isGIF = (image.sd_imageFormat == SDImageFormatGIF || [NSData sd_imageFormatForImageData:imageData] == SDImageFormatGIF);
if (!isGIF || isPlaceholder) {//不是动态图片,则正常显示
strongSelf.image = image;
strongSelf.animatedImage = nil;
dispatch_group_leave(group);
return;
}
FLAnimatedImage *animatedImage = SDWebImageCreateFLAnimatedImage(sstrongSelf, gifData);
sstrongSelf.image = animatedImage.posterImage;
sstrongSelf.animatedImage = animatedImage;
}
}
SDWebImage3.x与SDWebImage4.x主要的改动
新增的类
SDImageCacheConfig
SDWebImageDownloadToken
-
UIView (WebCache)
类新增方法sd_imageURL
sd_internalSetImageWithURL:placeholderImage:options:operationKey:setImageBlock:progress:completed:
sd_cancelCurrentImageLoad
sd_showActivityIndicatorView
sd_addActivityIndicator
sd_removeActivityIndicator
-
SDImageFormat
枚举 新增类型 (jpeg, png, gif, tiff, webp) -
FLAnimatedImageView (WebCache)
category forFLAnimatedImageView
from FLAnimatedImage
-
重命名的方法
-
setShowActivityIndicatorView:
renamed tosd_setShowActivityIndicatorView:
-
setIndicatorStyle:
renamed tosd_setIndicatorStyle:
- renamed
downloadImageWithURL:options:progress:completed
toloadImageWithURL:options:progress:completed
-
- renamed
SDWebImageCompletionBlock
toSDExternalCompletionBlock
- renamed
SDWebImageCompletionWithFinishedBlock
toSDInternalCompletionBlock
and added extraNSData
param - renamed
queryDiskCacheForKey:done:
toqueryCacheOperationForKey:done:
被废弃的方法
- removed the synchronous method
diskImageExistsWithKey:
- removed the synchronous
clearDisk
anddeleteOldFiles
- removed
removeImageForKey:
andremoveImageForKey:fromDisk:
- removed the deprecated method
contentTypeForImageData:
- removed deprecated methods:
imageURL
setImageWithURL:
setImageWithURL:placeholderImage:
setImageWithURL:placeholderImage:options:
setImageWithURL:completed:
setImageWithURL:placeholderImage:completed:
setImageWithURL:placeholderImage:options:completed:
setImageWithURL:placeholderImage:options:progress:completed:
sd_setImageWithPreviousCachedImageWithURL:andPlaceholderImage:options:progress:completed:
setAnimationImagesWithURLs:
cancelCurrentArrayLoad
cancelCurrentImageLoad
cachedImageExistsForURL:
diskImageExistsForURL:
SDWebImageCompletedBlock
SDWebImageCompletedWithFinishedBlock
downloadWithURL:options:progress:completed:
关于SDWebImage源码常见问题
SDWebImage4.0源码探究
SDWebImage 源码解析
SDWebImage源码阅读(上)
中文说明文档
下载器
gif
SDWebImage源码解析
SDWebImage源码解读