OC

SDWebImage源码分析

Posted by sunzhongliang on May 10, 2018

前言

SDWebImage是我们做iOS开发的时候一个比较常用的图片缓存加载库,对于一个优秀的三方库,就很有必要对它的源码进行阅读和学习,学习优秀的源码,还有助于提高我们的实力。

系统结构

一个优秀的框架应当具备"把简洁留给别人, 把复杂留给自己"特性,来看下它的系统结构设计 由系统结构图,将SDWebImage可以分为两类:

  • 核心类
    • SDWebImageManager 提供加载&取消图片以及缓存处理等,提供了加载图片的统一接口
    • SDImageCachesManager 负责SDWebImage的整个缓存工作,提供了缓存存储、删除、查找等功能。
    • SDImageLoaderManager 提供全局image loader管理
    • SDImageCodersManager 提供图片的解码工作,编码器数组是一个优先级队列,后面添加的编码器将具有最高优先级
    • SDWebImageDownloader 图片的下载中心,管理图片的下载队列
  • 工具类
    • UIButton+WebCache 支持UIButton加载图片的工具类
    • NSData+ImageContentType 根据图片数据获取图片的类型,比如GIF、PNG等
    • UIImage+MultiFormat 根据UIImage的data生成指定格式的UIImage
    • UIImage+GIF 传入一个GIF的NSData,生成一个GIF的UIImage
    • UIView+WebCache 所有的UIView及其子类都会调用这个分类的方法来完成图片加载的处理,同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作
    • SDAnimatedImageView+WebCache 提供SDAnimatedImageView.h加载GIF的能力(需使用SDAnimatedImageView)

SDWebImage加载图片时序

SDWebImage加载图片的顺序如下:

  • 组件调用sd_setImageWithURL, 最终都会执行到UIView+WebCachesd_internalSetImageWithURL:方法
- (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 {
    // 这里复制了一个context对象,避免外部改变影响到
    if (context) {
        // copy to avoid mutable object
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    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];
    self.sd_imageURL = url;
    // 如果options没有包含SDWebImageDelayPlaceholder(需要延时加载占位图)
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    if (url) {
        // reset the progress(重置progress进度)
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        [self sd_startImageIndicator];
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        } else {
            // 这里是一个处理循环引用的逻辑
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            SDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[SDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }
        // 处理加载进度
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        // 加载图片的逻辑
        @weakify(self);
        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) {
            @strongify(self);
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!self) { return; }
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // check whether we should use the image transition
            SDWebImageTransition *transition = nil;
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = self.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                callCompletedBlockClojure();
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
        [self sd_stopImageIndicator];
#endif
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
}
  • 进入到SDWebImageManagerloadImageWithURL:options:方法
  • 判断是否是黑名单url(多次加载失败就进入黑名单), 如果是则直接返回Image url is blacklisted
  • 进入到callCacheProcessForOperation:执行queryImageForKey方法查找缓存

      // Query normal cache process
      - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                      url:(nonnull NSURL *)url
                                  options:(SDWebImageOptions)options
                                  context:(nullable SDWebImageContext *)context
                                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
          // 获取要使用的图像缓存对象
          id<SDImageCache> imageCache;
          if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
              imageCache = context[SDWebImageContextImageCache];
          } else {
              imageCache = self.imageCache;
          }
            
          // 获取要使用的图像缓存类型(内存、磁盘)
          SDImageCacheType queryCacheType = SDImageCacheTypeAll;
          if (context[SDWebImageContextQueryCacheType]) {
              queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
          }
            
          // 查找缓存
          BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
          if (shouldQueryCache) {
              // 进入源码看到SDWebImage的缓存默认key是依据url来的, 但它也支持自定义SDWebImageCacheKeyFilter
              NSString *key = [self cacheKeyForURL:url context:context];
              @weakify(operation);
              operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
                  @strongify(operation);
                  if (!operation || operation.isCancelled) {
                      // Image combined operation cancelled by user
                      [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                      [self safelyRemoveOperationFromRunning:operation];
                      return;
                  } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
                      // Have a chance to quary original cache instead of downloading
                      [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                      return;
                  }
                    
                  // Continue download process
                  [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
              }];
          } else {
              // Continue download process
              [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
          }
      }
    
  • 进入到queryCacheOperationForKey方法查找缓存

      /**
      * Asynchronously queries the cache with operation and call the completion when done.
      *
      * @param key       The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`.
      * @param options   A mask to specify options to use for this cache query
      * @param context   A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
      * @param queryCacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediatelly.
      * @param doneBlock The completion block. Will not get called if the operation is cancelled
      *
      * @return a NSOperation instance containing the cache op
      */
      - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
    

    如果开启了内存缓存,那么先从内存里面查找缓存

SDWebImage缓存设计

SDWebImage的图片缓存采用的是 MemoryDisk双重Cache机制,通过SDImageCache来统一实现,其中Memory缓存是通过SDMemoryCache来实现的,Disk缓存是通过SDDiskCache来实现的。

SDMemoryCache

继承自NSCache,是线程安全的。

@interface SDMemoryCache <KeyType, ObjectType> ()
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; 
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; 
@end

可以看到定义了一个NSMapTable类型的weakCache以及一个信号量的锁.

缓存存储

// `setObject:forKey:` just call this with 0 cost. Override this is enough
- (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(self.weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        SD_UNLOCK(self.weakCacheLock);
    }
}

在添加缓存的时候,会先调用一下父类NSCache的setObject方法,然后再实现自身weakCache的缓存实现,为什么既然有了NSCache的缓存之后,这里还要再重复的实现一次缓存呢?
因为NSCache在收到内存警告时是无序释放内存的,并不是我们期望的先进先出形式,在实际情况下往往是新数据再次被利用的情况会很大,所以作者又加了一层缓存,并且做了一个开关shouldUseWeakMemoryCache控制,以此来提高缓存的命中率

缓存读取

- (id)objectForKey:(id)key {
    // 先从NSCache当中读取缓存
    id obj = [super objectForKey:key];
    // 如果没有开启shouldUseWeakMemoryCache缓存
    if (!self.config.shouldUseWeakMemoryCache) {
        return obj;
    }
    if (key && !obj) {
        // Check weak cache
        SD_LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        SD_UNLOCK(self.weakCacheLock);
        if (obj) {
            // Sync cache
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = [(UIImage *)obj sd_memoryCost];
            }
            // 重新把weakCache缓存存储到NSCache当中
            [super setObject:obj forKey:key cost:cost];
        }
    }
    // 返回weakCache取到的缓存
    return obj;
}

NSMapTable是存储可变元素的一种方式,可以通过弱引用来持有keys和values, 当key或者value被deallocated的时候,所存储的实体会被移除

SDDiskCache

SDDiskCache继承自NSObject,通过NSFileManager来实现磁盘缓存

  • 磁盘缓存是通过NSFileManager直接将图片存放到文件夹下面的
- (void)setData:(NSData *)data forKey:(NSString *)key {
    NSParameterAssert(data);
    NSParameterAssert(key);
    // 判断将要缓存的路径是否存在,如果不存在,则创建一个
    if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 获取默认的缓存路径
    NSString *cachePathForKey = [self cachePathForKey:key];
    // 将路径转为url
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    // 将图片数据写入文件,并保存
    [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        // 禁用icloud备份,默认是YES
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}
  • 磁盘缓存的key是MD5(url)+图片格式得到的
static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable 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]];
    // 所以最后的图片保存路径就是 "沙盒cache路径"+"url的md5吗"+".图片类型"
    return filename;
}

其它关于缓存的函数,移除过期的缓存或当缓存到最大值时移除较早的图片

- (void)removeExpiredData {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    
    // Compute content date key to be used for tests
    NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
    switch (self.config.diskCacheExpireType) {
        case SDImageCacheConfigExpireTypeAccessDate:
            cacheContentDateKey = NSURLContentAccessDateKey;
            break;
        case SDImageCacheConfigExpireTypeModificationDate:
            cacheContentDateKey = NSURLContentModificationDateKey;
            break;
        case SDImageCacheConfigExpireTypeCreationDate:
            cacheContentDateKey = NSURLCreationDateKey;
            break;
        case SDImageCacheConfigExpireTypeChangeDate:
            cacheContentDateKey = NSURLAttributeModificationDateKey;
            break;
        default:
            break;
    }
    
    NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
    
    // This enumerator prefetches useful properties for our cache files.
    NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                               includingPropertiesForKeys:resourceKeys
                                                                  options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             errorHandler:NULL];
    // 最早的有效缓存的时间,小于这个时间的缓存都失效了
    NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
    NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
    // 当前所有缓存的大小
    NSUInteger currentCacheSize = 0;
    
    // Enumerate all of the files in the cache directory.  This loop has two purposes:
    //
    //  1. Removing files that are older than the expiration date.
    //  2. Storing file attributes for the size-based cleanup pass.
    // 存储需要移除的缓存图片的路径
    NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
        NSError *error;
        NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
        
        // 错误处理
        if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
            continue;
        }
        
        // 通过时间来判断出需要移除的缓存
        NSDate *modifiedDate = resourceValues[cacheContentDateKey];
        if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }
        
        // 计算有效缓存大小
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
        cacheFiles[fileURL] = resourceValues;
    }
    // 移除缓存
    for (NSURL *fileURL in urlsToDelete) {
        [self.fileManager removeItemAtURL:fileURL error:nil];
    }
    
    // If our remaining disk cache exceeds a configured maximum size, perform a second
    // size-based cleanup pass.  We delete the oldest files first.
    NSUInteger maxDiskSize = self.config.maxDiskSize;
    // 当缓存大小大于设置的最多缓存控件时,移除相对较早缓存
    if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
        // 只留下剩下最大缓存的一半,其它全部清除了
        const NSUInteger desiredCacheSize = maxDiskSize / 2;
        
        // 通过缓存的时间来排序,才好移除早期的缓存
        NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                 usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                     return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                 }];
        
        // 开始清除缓存
        for (NSURL *fileURL in sortedFiles) {
            if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                // 当剩下的缓存小于最大缓存的一半时,停止缓存清除
                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
}

SDWebImage是通过removeExpiredData函数来移除过期的缓存或当缓存到最大值时移除较早的图片 当应用程序进入后台,会调用applicationDidEnterBackground函数执行移除 当应用程序终结的时候,会调用applicationWillTerminate函数执行移除

缓存配置

SDWebImage 为我们提供了一个专门配置的对象SDImageCacheConfig来设置内存缓存、缓存最大容量等

@interface SDImageCacheConfig : NSObject
// 是否对图片进行解压缩 默认 YSE
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 是否禁用icloud备份 默认 YSE
@property (assign, nonatomic) BOOL shouldDisableiCloud;
// 是否内存缓存 默认 YSE
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
/**
 * The reading options while reading cache from disk.
 * Defaults NSDataReadingMappedIfSafe
 */
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
/**
 * The writing options while writing cache to disk.
 * Defaults NSDataWritingAtomic
 */
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
// 缓存的超时时间
@property (assign, nonatomic) NSInteger maxCacheAge;
// 最大缓存容量
@property (assign, nonatomic) NSUInteger maxCacheSize;

@end

它是一个单例对象,只需我们再下载图片之前取到SDImageCache单例,就可以对其参数进行设置,如下

// 如果这几行代码写在 didFinishLaunchingWithOptions 里面,那么就可以对所有的图片下载进行设置
[SDImageCache sharedImageCache].config.maxCacheAge = 60 * 60 * 24 * 7; // 磁盘缓存 7天
[SDImageCache sharedImageCache].config.maxCacheSize = 0; // 磁盘缓存 这里设置为0,表示无限大
[SDImageCache sharedImageCache].config.shouldCacheImagesInMemory = true; // 开启内存缓存
[SDImageCache sharedImageCache].maxMemoryCost = 0; // 内存缓存 最大值
[SDImageCache sharedImageCache].maxMemoryCountLimit = 0; // 内存缓存 最大数量

本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.