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 *)context
{
  ...
}

在这个方法里我们可以看到里面有这样子一段代码:

id  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  : NSCache 
@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 *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的下载模块。

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

你可能感兴趣的:(iOS-SDWebImage底层框架解析)