SDWebImage是我们使用的最频繁的第三方之一,现在来阅读记录一下。
先看看SDWebImage里面有哪些类
再来介绍一下这些类
MKAnnotationView+WebCache.h 、UIButton+WebCache.h 、UIImageView+WebCache.h 这几个都是对应控件的网络图片加载
工具类
NSData+GIF.h 判断是否是GIF图片
UIImage+GIF.h 显示GIF动态图的动画
UIImage+MultiFormat.h 将二进制文件转成图片
SDWebImageCompat.h 清除网络图片中的@2x,@3x,防止图片加载失真
SDWebImageDecoder.h 对下载的图片进行解码处理
SDWebImagePrefetcher.h 预处理操作
缓存类
SDImageCache.h 图片缓存类
磁盘缓存地址默认在 沙盒/Library/Caches/bundleID/com.alamofire.imagedownloader/fsCachedData文件夹下
下载类
SDWebImageDownloader.h 网络图片下载器
SDWebImageDownloaderOperation.h 网络访问类
管理类
SDWebImageManager.h 图片下载的管理器
SDWebImageManager+MJ.h 根据url下载图片
协议类
SDWebImageOperation.h 取消访问的协议
SDImageCacheDelegate.h 图片缓存的代理类
SDWebImageDownloaderDelegate.h 网络图片下载代理
SDWebImageManagerDelegate.h 图片管理类代理
这里最重要的就是SDImageCache、SDWebImageDownloader、SDWebImageManager这几个类
先看看SDImageCache文件
1、SDImageCache的初始化方法,通过给定的名称初始化
- (id)initWithNamespace:(NSString *)ns
{
if ((self = [super init]))
{
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// Create IO serial queue
// 创建 串行队列
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// Init default values
// 初始化默认值
_maxCacheAge = kDefaultCacheMaxCacheAge;
// Init the memory cache
// 初始化内存缓存
_memCache = [[NSCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
// 初始化磁盘缓存
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
_diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace];
#if TARGET_OS_IPHONE
// Subscribe to app events
// 添加app事件通知
.....省略......
#endif
}
return self;
}
初始化方法里创建了一个串行队列、初始化了一个最大内存缓存空间大小
、初始化内存缓存对象、初始化磁盘缓存对象、添加内存警告通知、
程序进入后台和处于暂停状态通知。
2、缓存图片
- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk
{
if (!image || !key)
{
return;
}
[self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale];
if (toDisk)
{
dispatch_async(self.ioQueue, ^
{
NSData *data = imageData;
if (!data)
{
if (image)
{
#if TARGET_OS_IPHONE
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
}
if (data)
{
// Can't use defaultManager another thread
// 不能在其他线程使用单例
NSFileManager *fileManager = NSFileManager.new;
if (![fileManager fileExistsAtPath:_diskCachePath])
{
[fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
[fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
}
});
}
}
先把image缓存进内存,再判断如果需要缓存到磁盘,异步执行串行队列,把image转成二进制文件及其key值保存到_diskCachePath地址下
3、从缓存中取图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(void (^)(UIImage *image, SDImageCacheType cacheType))doneBlock
{
NSOperation *operation = NSOperation.new;
if (!doneBlock) return nil;
if (!key)
{
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image)
{
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
dispatch_async(self.ioQueue, ^
{
if (operation.isCancelled)
{
return;
}
@autoreleasepool
{
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage)
{
CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_main_sync_safe(^
{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
首先根据key值去内存中寻找对应的缓存,如果找到,则返回图片。
其次开启异步串行队列,去磁盘的缓存中寻找,如果找到,把图片缓存进内存中,并且回主线程返回图片
SDWebImageDownloader文件
1、初始化
- (id)init
{
if ((self = [super init]))
{
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = NSOperationQueue.new;
_downloadQueue.maxConcurrentOperationCount = 2;
_URLCallbacks = NSMutableDictionary.new;
_HTTPHeaders = [NSMutableDictionary dictionaryWithObject:@"image/webp,image/*;q=0.8" forKey:@"Accept"];
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
初始化一个先进先出的队列、一个网络请求队列、设置最大并行下载数为2、访问网络url的字典、HTTP请求的header、并行队列_barrierQueue
2、这一步是以url为key把progressBlock和completedBlock保存到URLCallbacks字典中
- (void)addProgressCallback:(void (^)(NSUInteger, long long))progressBlock andCompletedBlock:(void (^)(UIImage *, NSData *data, NSError *, BOOL))completedBlock forURL:(NSURL *)url createCallback:(void (^)())createCallback
{
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
// 该url将作为callbacks的key ,不能为nil。如果url是nil completed block直接返回空的图片或数据
if(url == nil)
{
if (completedBlock != nil)
{
completedBlock(nil, nil, nil, NO);
}
return;
}
//栅栏函数,分割同步线程之前和之后的代码
dispatch_barrier_sync(self.barrierQueue, ^
{
BOOL first = NO;
if (!self.URLCallbacks[url])
{
self.URLCallbacks[url] = NSMutableArray.new;
first = YES;
}
// Handle single download of simultaneous download request for the same URL
// 操作单独一个下载任务并且同时请求相同的url
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = NSMutableDictionary.new;
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first)
{
createCallback();
}
});
}
栅栏函数,确保之后的代码是最新的,如果self.URLCallbacks字典中保存url,则创建一个url对应的数组,并说明是初次创建url数组,把progressBlock和completedBlock保存到url对应的数组中,根据前面的说明,回调新创建了url的数组。
3、下载图片
- (id<SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(void (^)(NSUInteger, long long))progressBlock completed:(void (^)(UIImage *, NSData *, NSError *, BOOL))completedBlock
{
__block SDWebImageDownloaderOperation *operation;
__weak SDWebImageDownloader *wself = self;
//url作为key值把progressBlock(进度回调) completedBlock(完成回调)保存到_URLCallbacks字典中,如果没有之前url对应的value没有数据,则回调createCallback
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^
{
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
// 创建request ,防止我们已经设置好的缓存策略
NSMutableURLRequest *request = [NSMutableURLRequest.alloc initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:15];
request.HTTPShouldHandleCookies = NO;
request.HTTPShouldUsePipelining = YES;
//设置请求头
request.allHTTPHeaderFields = wself.HTTPHeaders;
//创建一个下载请求操作
operation = [SDWebImageDownloaderOperation.alloc initWithRequest:request options:options progress:^(NSUInteger receivedSize, long long expectedSize)
{
if (!wself) return;
SDWebImageDownloader *sself = wself;
NSArray *callbacksForURL = [sself callbacksForURL:url];
for (NSDictionary *callbacks in callbacksForURL)
{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
//返回下载进度和预计大小
if (callback) callback(receivedSize, expectedSize);
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished)
{
if (!wself) return;
SDWebImageDownloader *sself = wself;
NSArray *callbacksForURL = [sself callbacksForURL:url];
if (finished)
{
//如果下载完成了,移除url的所有block回调绑定
[sself removeCallbacksForURL:url];
}
for (NSDictionary *callbacks in callbacksForURL)
{
//调用完成下载的回调
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^
{
if (!wself) return;
SDWebImageDownloader *sself = wself;
//取消下载,移除url的所有block回调绑定
[sself removeCallbacksForURL:url];
}];
//把这个下载操作加入到下载队列去
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder)
{
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
// 模仿后进先出执行顺序系统地添加新的操作放在最后的操作里
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
把progressBlock和completedBlock保存到url对应的数组中。如果是初次创建url的数组,根据url创建一个网络请求的NSMutableURLRequest,做一些设置,调用SDWebImageDownloaderOperation文件中的网络请求方法的block回调,把返回图片下载进度返回出去,在请求完成时,移除url对应的block数组,并调用这些block回调把UIImage传出去。
(多种相同url的图片同时请求时,只有第一次会创建网络请求,后续的都会把block保存到url字典里的数组中,当请求结束时,依次调用block返回获取的图片,并把url对应的数组删除)
SDWebImageManager文件
1、初始化
- (id)init
{
if ((self = [super init]))
{
_imageCache = [self createCache];
_imageDownloader = SDWebImageDownloader.new;
_failedURLs = NSMutableArray.new;
_runningOperations = NSMutableArray.new;
}
return self;
}
初始化图片缓存类、图片下载类、失败url数组、运行中的网络访问数组。
2、加载图片操作
- (id<SDWebImageOperation>)downloadWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletedWithFinishedBlock)completedBlock
{
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
// 这是非常常见的错误 用NSString对象而不是NSURL对象发送URL
// 因为一些奇怪的原因,Xcode不会因为这个错误抛出任何警告。这里我们要安全处理这个允许NSString作为URLs是很不安全的错误
if ([url isKindOfClass:NSString.class])
{
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 防止 app因为url的类型问题而闪退,比如是NSNull类型而不是NSURL
if (![url isKindOfClass:NSURL.class])
{
url = nil;
}
__block SDWebImageCombinedOperation *operation = SDWebImageCombinedOperation.new;
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
@synchronized(self.failedURLs)
{
isFailedUrl = [self.failedURLs containsObject:url];
}
if (!url || !completedBlock || (!(options & SDWebImageRetryFailed) && isFailedUrl))
{
if (completedBlock)
{
dispatch_main_sync_safe(^
{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES);
});
}
return operation;
}
@synchronized(self.runningOperations)
{
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
//去缓存里找到缓存的图片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType)
{
if (operation.isCancelled)
{
@synchronized(self.runningOperations)
{
[self.runningOperations removeObject:operation];
}
return;
}
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
{
.....省略......
//从网络下载图片
id<SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)
{
if (weakOperation.isCancelled)
{
dispatch_main_sync_safe(^
{
completedBlock(nil, nil, SDImageCacheTypeNone, finished);
});
}
else if (error)
{
dispatch_main_sync_safe(^
{
completedBlock(nil, error, SDImageCacheTypeNone, finished);
});
if (error.code != NSURLErrorNotConnectedToInternet)
{
@synchronized(self.failedURLs)
{
[self.failedURLs addObject:url];
}
}
}
else
{
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage)
{
// Image refresh hit the NSURLCache cache, do not call the completion block
//刷新NSURLCache缓存中的图片,不回调completion block
}
// NOTE: We don't call transformDownloadedImage delegate method on animated images as most transformation code would mangle it
// 笔记:如果是动态图我们不会执行transformDownloadedImage的代理方法,因为会执行太多转换代码导致图片损坏
else if (downloadedImage && !downloadedImage.images && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)])
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^
{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
dispatch_main_sync_safe(^
{
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished);
});
if (transformedImage && finished)
{
NSData *dataToStore = [transformedImage isEqual:downloadedImage] ? data : nil;
[self.imageCache storeImage:transformedImage imageData:dataToStore forKey:key toDisk:cacheOnDisk];
}
});
}
else
{
dispatch_main_sync_safe(^
{
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished);
});
if (downloadedImage && finished)
{
[self.imageCache storeImage:downloadedImage imageData:data forKey:key toDisk:cacheOnDisk];
}
}
}
if (finished)
{
@synchronized(self.runningOperations)
{
[self.runningOperations removeObject:operation];
}
}
}];
operation.cancelBlock = ^{[subOperation cancel];};
}
else if (image)
{
dispatch_main_sync_safe(^
{
completedBlock(image, nil, cacheType, YES);
});
@synchronized(self.runningOperations)
{
[self.runningOperations removeObject:operation];
}
}
else
{
// Image not in cache and download disallowed by delegate
dispatch_main_sync_safe(^
{
completedBlock(nil, nil, SDImageCacheTypeNone, YES);
});
@synchronized(self.runningOperations)
{
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
}
先处理几种NSURL的非正常情况,然后调用的是SDImageCache解析的第三个方法:从缓存中取图片(先去内存缓存中找,没找到再去磁盘缓存中找,若取到了则把图片缓存到内存中)。
1、没找到图片或者设置取消刷新 与代理没有执行时(若有图片并且取消刷新则先把图片返回),调用SDWebImageDownloader解析的第三个方法:下载图片,处理各种非正常情况,把成功下载的图片回调,然后保存图片进缓存中。
2、如果找到图片,则回调block返回图片。
以上三个最重要的类里面核心的几个方法都分析完了,下面用UIImageView+WebCache.h里的加载图片的方法,把SDWebImage加载图片的流程走一遍
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletedBlock)completedBlock
{
[self cancelCurrentImageLoad];
self.image = placeholder;
if (url)
{
__weak UIImageView *wself = self;
id<SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (!wself) return;
dispatch_main_sync_safe(^
{
__strong UIImageView *sself = wself;
if (!sself) return;
if (image)
{
sself.image = image;
[sself setNeedsLayout];
}
if (completedBlock && finished)
{
completedBlock(image, error, cacheType);
}
});
}];
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
先删除重复的网络请求,把占位图设置到UIImageView上,确认url存在后通过SDWebImageManager调用加载图片操作,以这个url为key先去内存缓存中找对应的图片,找到了就返回图片,没找到去磁盘缓存中找,磁盘缓存中找到了也返回图片,并把这张图片保存到内存缓存中,没找到再调用网络访问url,下载图片,下载成功后,返回图片并缓存到内存和磁盘中。
UIImageView接收到图片后,设置成当前的图片。
网络图片就这有加载好了。