美文网首页Mac·iOS开发IT/互联网
iOS-SDWebImage底层框架解析

iOS-SDWebImage底层框架解析

作者: 夜里昙花现 | 来源:发表于2019-12-16 19:12 被阅读0次

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

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

    相关文章

      网友评论

        本文标题:iOS-SDWebImage底层框架解析

        本文链接:https://www.haomeiwen.com/subject/ngednctx.html