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的下载模块。

此致,谢谢博友们看完,如有不足,欢迎指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容