SDWebImage 5.0以后,項目中的代碼文件非常多,這邊對文件進行了一下簡單區(qū)分,代碼結構如下。

結構圖.png
主要分為一下幾塊
1、Render渲染
2、Coder編碼解碼
3、AnimatedImage動圖加載
4、Utils工具類
5、Cache緩存
6、Downloader下載
7、Category相關類使用接口
今天主要講一下Cache這部分
- 磁盤存儲放在一個串行隊列中
- 創(chuàng)建內(nèi)存存儲和磁盤存儲的Cache
- 老版本升級圖片轉移
- 退到后臺或者殺掉APP,刪除老的緩存
///單利
@property (nonatomic, class, readonly, nonnull) SDImageCache *sharedImageCache
///實現(xiàn)
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nullable NSString *)directory
config:(nullable SDImageCacheConfig *)config {
if ((self = [super init])) {
NSAssert(ns, @"Cache namespace should not be nil");
// 創(chuàng)建串行隊列
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
if (!config) {
config = SDImageCacheConfig.defaultCacheConfig;
}
_config = [config copy];
// Init the memory cache
NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol");
// 內(nèi)存緩存
_memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
// Init the disk cache
if (!directory) {
// 默認存儲路徑
directory = [self.class defaultDiskCacheDirectory];
}
_diskCachePath = [directory stringByAppendingPathComponent:ns];
NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol");
// 磁盤緩存
_diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config];
// 版本升級圖片轉移
// 新 ~/Library/Caches/com.hackemist.SDImageCache/default/
// 老 ~/Library/Caches/default/com.hackemist.SDWebImageCache.default/
[self migrateDiskCacheDirectory];
#if SD_UIKIT
// 退到后臺或者殺掉APP,刪除老的緩存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
#if SD_MAC
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:NSApplicationWillTerminateNotification
object:nil];
#endif
}
return self;
}
圖片存在到內(nèi)存和磁盤
- 內(nèi)存存儲
- 磁盤存儲,異步存儲(串行隊列)
- 數(shù)據(jù)編碼
- 通過NSKeyedArchiver存儲擴展信息,key和圖片相同
/// 圖片存在到內(nèi)存和磁盤
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// 內(nèi)存存儲
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
// 磁盤存儲
dispatch_async(self.ioQueue, ^{
// 異步存儲(串行隊列)
@autoreleasepool {
NSData *data = imageData;
if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// If image is custom animated image class, prefer its original animated data
data = [((id<SDAnimatedImage>)image) animatedImageData];
}
if (!data && image) {
// Check image's associated image format, may return .undefined
SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined) {
// If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
if (image.sd_isAnimated) {
format = SDImageFormatGIF;
} else {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
}
}
// 數(shù)據(jù)編碼
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
if (image) {
// 通過NSKeyedArchiver存儲擴展信息,key和圖片相同
// Check extended data
id extendedObject = image.sd_extendedObject;
if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) {
NSData *extendedData;
if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
NSError *error;
extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
if (error) {
NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
}
} else {
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
#pragma clang diagnostic pop
} @catch (NSException *exception) {
NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
}
}
if (extendedData) {
[self.diskCache setExtendedData:extendedData forKey:key];
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
查詢圖片
- 首先從內(nèi)存中查詢
- 對需要解碼和動圖處理
- 從磁盤中查詢
- 內(nèi)存中不存在,從磁盤解碼出圖片數(shù)據(jù)并存入磁盤中
// 查詢圖片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// Invalid cache type
if (queryCacheType == SDImageCacheTypeNone) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 首先從內(nèi)存中查詢
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
}
if (image) {
// 對需要解碼和動圖處理
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
Class animatedImageClass = image.class;
if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
} else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}
BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 從磁盤中查詢
NSOperation *operation = [NSOperation new];
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
if (image) {
// the image is from in-memory cache, but need image data
diskImage = image;
} else if (diskData) {
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
}
};
// Query in ioQueue to keep IO-safe
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
SDMemoryCache 內(nèi)存緩存
這個對象是繼承自NSCache的。同時這個支持弱存儲。
NSCache是通過下面2個方法實現(xiàn)的
- (void)setObject:(nullable id)object forKey:(nonnull id)key;
- (void)removeObjectForKey:(nonnull id)key;
弱存儲主要是通過NSMapTable,因為圖片存儲是多線程的,所以這邊對weakCache的操作都進行了加鎖處理(SD_LOCK)。
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *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);
}
}
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
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];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
SDDiskCache 磁盤存儲
磁盤存儲主要是通過NSFileManager對數(shù)據(jù)進行存儲和刪除
移除過期的磁盤數(shù)據(jù)
/// NSFileManager的方法,可以獲取到含有時間信息的URL的枚舉數(shù)據(jù)
- (nullable NSDirectoryEnumerator<NSURL *> *)enumeratorAtURL:(NSURL *)url includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys options:(NSDirectoryEnumerationOptions)mask errorHandler:(nullable BOOL (^)(NSURL *url, NSError *error))handler API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
/// 字典進行排序
- (NSArray<KeyType> *)keysSortedByValueWithOptions:(NSSortOptions)opts usingComparator:(NSComparator NS_NOESCAPE)cmptr API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
- (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];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
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) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = maxDiskSize / 2;
// Sort the remaining cache files by their last modification time or last access time (oldest first).
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
// Delete files until we fall below our desired cache size.
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;
}
}
}
}
}