4个模块
- SDWebImageManager管理器
- 缓存机制
- 下载管理
- UIKit模块

工作流程

关键类
SDWebImageManager的核心,负责发起下载和缓存。
SDImageCeche,缓存对象,负责内存和沙盒的缓存。
SDWebImageDownloader负责所有图片的下载操作。
SDWebImageDownloaderOperation负责单个文件的下载。
SDWebImageManager图片加载的流程
-
downloadImageWithURL:options:progress:completed: 中会先拿图片缓存的 key 去 SDImageCache 中读取内存缓存,如果有,就返回给 SDWebImageManager;如果内存缓存没有,就开启异步线程,拿经过 MD5 处理的key 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存中去,然后再返回给 SDWebImageManager。
-
如果内存缓存和磁盘缓存中都没有,SDWebImageManager 就会调用 SDWebImageDownloader的方法去下载
-downloadImageWithURL: options: progress: completed:
该会先将传入的 progressBlock 和 completedBlock 保存起来,并在第一次下载该 URL 的图片时,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloader 的downloadQueue 来启动异步下载任务,使用了NSOperationQueue这种多线程,更加对象化,可取消图片的下载操作。
-
SDWebImageDownloaderOperation 中包装了一个 NSURLSession的网络请求,在URLSession的回调中拿到图片数据,开始异步解码操作,并最终回调 completedBlock。
-
SDWebImageDownloaderOperation 中的图片下载请求完成后,会回调给 SDWebImageDownloader,然后 SDWebImageDownloader 再回调给 SDWebImageManager,SDWebImageManager 中再将图片分别缓存到内存和磁盘上,GCD异步到回到主线程中设置UIImageView的 image 属性。
SDImageCeche清理缓存
- 初始化NSCache的时候,注册了通知,收到了内存警告就清理,最大存储时间是7天,到了也清理。
- 分为clearMemory,clearDisk.
- 清理磁盘流程,先找到磁盘目录,遍历创建文件目录的枚举器,找到过期的文件,添加到一个数组中,没有过期的文件,以键值形式存到一个字典中。
- 遍历删除过期的文件。
- 再检测磁盘的缓存大小,超过最大缓存值,把上一步缓存的文件根据修改时间排序,遍历删除,直到最大缓存大小的一半(时间最早的最先删除)。
SDWebImageDownloader下载
最大并发数,在SDWebImageDownloaderConfig中默认了
_maxConcurrentDownloads = 6;
_downloadTimeout = 15.0;
关键代码分析
- UIKit调用sd的时候都会走到UIView+WebCache中的,为什么都走这里呢,方便统一管理取消任务
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
第一步:判断这个View上有没有在执行任务,经常出现在UITableview中
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
如果有,取消当前任务,通过当前view的key,这个可以值存在UIView+WebCacheOperation中
- (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];
}
}
}
}
- (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;
}
}
添加operation,在
- (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];
}
}
}
}
这个过程保证了一个UIImageView只会添加一次任务。
id <SDWebImageOperation> operation类型,它是一个
-
接下来就是初始化SDWebImageManager,还有一些进度的block
-
进入管理类的loadImageWithURL方法,有缓存就加载缓存,没缓存就网络获取。在completed回调中可以看出SDImageCacheType
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
这一步主要看SDWebImageCache类,怎么获取缓存图片,存储图片,管理磁盘内存。清理缓存看上面介绍的SDImageCeche清理缓存
- 内存缓存的具体操作,内存缓存中SDMemoryCache有一段存取的代码
- (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) {
// Store weak cache
SD_LOCK(_weakCacheLock);
[self.weakCache setObject:obj forKey:key];
SD_UNLOCK(_weakCacheLock);
}
}
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
SD_LOCK(_weakCacheLock);
obj = [self.weakCache objectForKey:key];
SD_UNLOCK(_weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = [(UIImage *)obj sd_memoryCost];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
SDMemoryCache重写了NSCache的存取方法
问题:
- SDMemoryCache继承系统的NSCache
- 但是发现缓存中使用的self.weakCache是NSMapTable,而且存储的时候调用了父类的set方法 [super setObject:obj forKey:key cost:g];
- 这样内存缓存中就有两份缓存了,这是为什么?
- 系统缓存可能随时被清理,所以存储的时候存储两份,一份系统,一份自己维护
- 先去系统NSCache中去,没有就在self.weakCache中找,找到了再次[super setObject:obj forKey:key cost:cost];
- 提高了查找速度
- SDImageCeche缓存中使用GCD的异步串行解码、计算尺寸大小、删除文件操作
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// If image is custom animated image class, prefer its original animated data
data = [((id<SDAnimatedImage>)image) animatedImageData];
}
if (!data && image) {
// Check image's associated image format, may return .undefined
SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined) {
// If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
if (image.sd_isAnimated) {
format = SDImageFormatGIF;
} else {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
}
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
[self _archivedDataWithImage:image forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
网友评论