iOS-SDWebImage底层框架解析
SDWebImage是iOS开发中一个常用的图片第三方框架,我们常会这样子在ImageView上去加载一张网络图片
[_imageView sd_setImageWithURL:[NSURL URLWithString:@"图片url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
那你知道它加载图片的过程吗?
下面,我们先来看看SDWebImage官方是怎么解释这个框架的。
(如果点不开查看这里:https://github.com/SDWebImage/SDWebImage)
This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like UIImageView, UIButton, MKAnnotationView.
这个库提供了一个支持缓存的异步图像下载器。为了方便,我们为UI元素添加了类别,比如UIImageView, UIButton, MKAnnotationView。
官方的解释很简洁,就是一个支持缓存的一部图像下载器,同时对UIKit做了一些扩展,方便使用。
我们通过上面链接下载了SDWebImage,大体看了下整个库,可以分为四部分:
- 第一部分:SDWebImageManager,也就是整个SDWebImage的管理类;
- 第二部分:SDWebImage扩展(UIKit的扩展),方便我们进行调用,比如上面说的,加载网络图片,我们可以通多sd_...去使用;
- 第三部分:SDWebImageDownloader,顾名思义,就是图片下载;
-
第四部分:SDWebImageCache,也是就是图片缓存类。
具体我们可以看下下面这张图来了解一下:
SDWebImage库类图
到这里,我们大概的了解了SDWebImage整个框架。那回到之前的问题,它是怎么去加载一张网络图片的呢?
-
网络图片的加载流程
下面,我们打开SDWebImage的代码一起来看下SDWebImage加载图片是实现的。
这里,我们以UIImageView为例,我们通过UIImageView+WebCache.h的sd_...方法一直点进去来到UIView+WebCache.m的sd_internalSetImageWithURL...的方法里
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
internalSetImageBlock:(nullable SDInternalSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context
{
...
}
在这个方法里我们可以看到里面有这样子一段代码:
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
...
}];
我们从这里再点进去可以看到
//通过key查询缓存中
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
...
}];
到这里,你有没有看到到一个方法[queryCacheOperationForKey...],如果你注意到了,这会儿是不是有一点点小明白了,别急,我们继续往下看,你是不是迫不及待的想从这个方法点进去看看它到底做了哪些操作,那我们一起来看看
//先从内存中查找图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
//如果内存中没有找到,再从磁盘中查找
void(^queryDiskBlock)(void) = ^{
...
@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 = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
...
}
};
/**
tips:
这里为什么要要使用autoreleasepool呢?
因为这里会产生大量的临时变量,使用autoreleasepool可以更快的进行释放
*/
看到这里,你可能会疑惑,如果内存缓存和磁盘缓存中没有图片,SDWebImage又是怎么去处理的呢?
还能怎么处理,当然是去下载啦,我们回到上一层,也就是查询缓存的方法,来看看它的回调中又是怎么去操作的。
//通过key查询缓存中
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
...
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
...
//进行图片下载
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
...
//下载完成后对图片进行存储
if (downloadedImage && finished) {
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
});
} else {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
}];
...
}
}];
结束,整个图片的加载流程就是这样子的啦,是不是明白了?
我们来总结一下网络图片加载过程:查询图片缓存(内存缓存和磁盘缓存),如果在缓存中找不到图片,则调起网络接口进行图片下载并返回图片,除此之外,还需将图片保存到内存缓存和磁盘缓存中。
这里有一个值得注意的地方,SDWebImage是怎么将图片存储在内存缓存中,而且,为什么还要自己实现一个内存缓存类(SDMemoryCache),直接用NSCache不好吗?
-
缓存讲解
了解了网络图片的加载过程,又出现了两个新的问题,那再让我们一起来看一看,今天,我们就彻底把它们弄清楚!
第一个问题先放放,我先看第二个问题
为什么要SDWebImage要自己实现一个内存缓存类SDMemoryCache?
答:我们通过SDMemoryCache.m可以看到
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
@end
它是继承自NSCache。我们知道,NSCache能够操作缓存,但它有一个问题,内存中的缓存数据什么时候清理不归NSCache管理,所以,当数据很多的时候,在下一个取值的时候,我们就没办法取到缓存了,所以,SDWebImage才会自己实现一个内存缓存类。
在SDWebImageCache.m中我们可以看到这样一段代码:
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
...
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memCache setObject:image forKey:key cost:cost];
}
...
}
//---SDImageCacheConfig.h---
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory; //默认值为YES
看到这里,我们是不是都明白了?也许会有人问,这样子岂不是有两分内存缓存?
答案是可能会有,换句话说,如果当我们通过objectForKey:去获取图片的时候,如果值为空,而我们又shouldUseWeakMemoryCache为YES,我们这时候可以直接拿到这个图片,不用再去请求一次,也就是以空间换区时间。
以上,也就是为什么SDWebImage要自己去实现一个内存缓存类的原因了。
这里,我们回到第一个问题,SDWebImage是怎么将图片存储在缓存中的?
我们再来看看上面那个方法
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
...
//内存缓存
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memCache setObject:image forKey:key cost:cost];
}
//磁盘缓存
if (toDisk) {
...
[self _storeImageDataToDisk:data forKey:key];
}
}
呐,大体就是这样子的,但看到这里,总感觉有点似懂非懂的样子?
那我们再来看看memCache它是什么?
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
//
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
NSMapTable?它是什么?
通过查找资料,我们了解到,NSMapTable有点类似于NSDictionary,只不过NSMapTable比NSDictionary提供了更多的内存语义。
通过上面代码我们可以看到,NSMapTable在alloc的时候,对key进行了strong设置,对value进行了weak设置,所以,当我们的对象被释放的时候,NSMapTable会自动删除key-value。
Tips:
NSMapTable 内存语义:assgin,copy,strong
NSDictionary 内存语义:NSCoping
- (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
LOCK(self.weakCacheLock);
// Do the real copy of the key and only let NSMapTable manage the key's lifetime
// Fixes issue #2507 https://github.com/SDWebImage/SDWebImage/issues/2507
[self.weakCache setObject:obj forKey:[[key mutableCopy] copy]];
UNLOCK(self.weakCacheLock);
}
}
看完这个,是不是豁然开朗,哈哈
最后,我们再来看一个磁盘缓存一个小小的点
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
...
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];
...
}
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (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;
}
磁盘缓存:
传建一个目录,为每一个缓存文件生成一个MD5文件名。
那SDWebImage今天就说道这里了,后面如果有时间,会围绕SDWebImageDownloader和SDWebImageDownloaderOperation来谈一谈SDWebImage的下载模块。
此致,谢谢博友们看完,如有不足,欢迎指正。