SDWebImage緩存部分實(shí)現(xiàn)源碼解析

SDWebImage主要使用SDImageCache來緩存圖片,實(shí)現(xiàn)了內(nèi)存存取和磁盤存取還有一系列的處理。下面分析它的源碼。本文分析的版本為4.4.3。首先來看一下它對(duì)開發(fā)者暴露的接口。

屬性

首先我們來看一下它的屬性

#pragma mark - Properties
//配置
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
//最大內(nèi)存大小
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//最大內(nèi)存數(shù)量
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

這3個(gè)屬性都是可配置的屬性,其中maxMemoryCost和maxMemoryCountLimit用于配置其內(nèi)部的NSCache,config則負(fù)責(zé)大部分的配置,下面是它內(nèi)部的屬性。

@interface SDImageCacheConfig : NSObject
//是否解壓縮圖片,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否禁用iCloud備份,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//是否緩存一份到內(nèi)存中,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//是否額外存一份弱引用的緩存,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
//從磁盤讀取圖片的配置項(xiàng),默認(rèn)是NSDataReadingMappedIfSafe,也就是使用文件映射內(nèi)存的方式,是不消耗內(nèi)存的
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
//寫文件的配置項(xiàng),默認(rèn)是NSDataWritingAtomic,也就是會(huì)覆蓋原有的文件
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
//圖片在磁盤的最大時(shí)間,默認(rèn)是一周
@property (assign, nonatomic) NSInteger maxCacheAge;
//圖片在磁盤的最大大小,默認(rèn)是0,即沒有限制
@property (assign, nonatomic) NSUInteger maxCacheSize;
//清除磁盤緩存是基于什么清除,默認(rèn)是SDImageCacheConfigExpireTypeModificationDate,即基于圖片修改時(shí)間
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
@end

可以看出SDImageCacheConfig中大多數(shù)配置都是跟磁盤相關(guān)的。

初始化方法

//單例
+ (nonnull instancetype)sharedImageCache;
//新建一個(gè)存儲(chǔ)類,如果是用init方法創(chuàng)建,默認(rèn)傳入的是default
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;
//全能初始化方法,比起上一個(gè)方法,額外指定了存儲(chǔ)目錄,默認(rèn)目錄是在Cache/default的文件夾下
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

其全能初始化方法的實(shí)現(xiàn)如下:

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
        //創(chuàng)建專門讀寫磁盤的隊(duì)列,注意是并發(fā)
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        //初始化配置config
        _config = [[SDImageCacheConfig alloc] init];
        //初始化內(nèi)存空間
        _memCache = [[SDMemoryCache alloc] initWithConfig:_config];
        _memCache.name = fullNamespace;
        //初始化存儲(chǔ)目錄
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }
        dispatch_sync(_ioQueue, ^{
            self.fileManager = [NSFileManager new];
        });
        //注冊(cè)通知,大意就是在程序進(jìn)后臺(tái)和退出的時(shí)候,清理一下磁盤
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
    }
    return self;
}

其中,makeDiskCachePath也是個(gè)暴露的方法:

- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
    NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

顯然文件的目錄基本都在Cache目錄下。

內(nèi)存存儲(chǔ)設(shè)計(jì)

這個(gè)類是使用SDMemoryCache來進(jìn)行存儲(chǔ),它繼承自NSCache,而在這個(gè)類中,又持有了一個(gè)weakCache屬性弱引用著內(nèi)存,它實(shí)際上是在config中的shouldUseWeakMemoryCache置為YES才有效的。

@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
@end
@interface SDMemoryCache <KeyType, ObjectType> ()
//配置
@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
//弱引用緩存
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; 
//信號(hào)量的鎖
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; 
- (instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
@end

首先里面的NSMapTable相當(dāng)于一個(gè)字典,他的相關(guān)知識(shí)可以參看這篇文章,總的來說,它可以設(shè)置鍵和值是賦值方式,當(dāng)設(shè)置鍵的賦值方式為Copy,值的賦值方式為Strong的時(shí)候,它就相當(dāng)于NSMutableDictionary。

它自身也是一個(gè)NSCache,但與父類不一樣的是,它多了一個(gè)收到內(nèi)存警告,刪除父類所有對(duì)象的功能。

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}

- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
    self = [super init];
    if (self) {
        //weakCache是一個(gè)鍵是強(qiáng)引用,值是弱引用的MapTable
        self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        self.weakCacheLock = dispatch_semaphore_create(1);
        self.config = config;
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarning:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
    }
    return self;
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    //移除父類的對(duì)象
    [super removeAllObjects];
}

可以看出,在收到內(nèi)存警告的時(shí)候,僅僅清除了父類的對(duì)象,并沒有清除weakCache的對(duì)象,因?yàn)槭侨跻妙愋?,也不用手?dòng)清除。

接下來就是一些操作內(nèi)存的時(shí)候?qū)eakCache的一些同步操作方法:

//setObject的相關(guān)方法都會(huì)調(diào)到這里來,因此只需重寫這個(gè)方法
- (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) {
        LOCK(self.weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        UNLOCK(self.weakCacheLock);
    }
}

- (id)objectForKey:(id)key {
    //先看看自身有沒有這個(gè)值
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return obj;
    }
    if (key && !obj) {
        //從緩存找,找到的話重新設(shè)置回去
        LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        UNLOCK(self.weakCacheLock);
        if (obj) {
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = SDCacheCostForImage(obj);
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}
//移除的時(shí)候,weakCache也需要同步移除
- (void)removeObjectForKey:(id)key {    
    [super removeObjectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key) {
        LOCK(self.weakCacheLock);
        [self.weakCache removeObjectForKey:key];
        UNLOCK(self.weakCacheLock);
    }
}

- (void)removeAllObjects {
    [super removeAllObjects];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    LOCK(self.weakCacheLock);
    [self.weakCache removeAllObjects];
    UNLOCK(self.weakCacheLock);
}

這一塊代碼容易讀懂,主要思想是在NSCache因?yàn)槟承┰蚯宄臅r(shí)候在內(nèi)存中仍然維持著一份弱引用,只要這些弱引用的對(duì)象仍然被其他對(duì)象(比如UIImageView)所持有,那仍然會(huì)在該類中找到。

雖然這里引入了SDImageCacheConfig,但是實(shí)際上只使用了它的shouldUseWeakMemoryCache屬性,雖然代碼看上去并沒有直接設(shè)置shouldUseWeakMemoryCache這個(gè)成員屬性來得好,但是以后擴(kuò)展起來會(huì)容易一些。

存儲(chǔ)圖片方法

這個(gè)文件暴露了很多存圖片的API,大部分都會(huì)調(diào)到下面的方法中來:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        //沒有圖片和存儲(chǔ)鍵直接返回
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    //內(nèi)存存一份
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
                //這里的data比較大,存到磁盤后需要及時(shí)釋放掉,不能讓其繼續(xù)占用內(nèi)存
            @autoreleasepool {
                NSData *data = imageData;
                if (!data && image) {
                    //如果data為nil,則轉(zhuǎn)換為data存儲(chǔ)
                    SDImageFormat format;
                    if (SDCGImageRefContainsAlpha(image.CGImage)) {
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                //磁盤存一份,這個(gè)方法會(huì)阻塞線程
                [self _storeImageDataToDisk:data forKey:key];
            }
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
        [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    //返回MD5后的字符串,這一步也是耗時(shí)的
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //寫文件
    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    if (self.config.shouldDisableiCloud) {
        //不讓該文件被iCloud備份
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

所有關(guān)于磁盤的耗時(shí)操作都放在ioQueue里操作,這樣保證了主線程的正常運(yùn)行。

獲取圖片方法

讀取圖片的方法有很多,其中先從最常用的方法講起:

- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
    //先從緩存讀
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }
    //緩存找不到,就從磁盤找
    image = [self imageFromDiskCacheForKey:key];
    return image;
}

其中,imageFromMemoryCacheForKey這個(gè)方法比較簡(jiǎn)單,無非就是從memCache中讀取而已,這里就不貼代碼了。

imageFromDiskCacheForKey這個(gè)方法的實(shí)現(xiàn)如下:

- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        //重新將圖片放進(jìn)內(nèi)存
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }
    return diskImage;
}

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    //先讀取出數(shù)據(jù)
    NSData *data = [self diskImageDataForKey:key];
    //再將數(shù)據(jù)轉(zhuǎn)成圖片
    return [self diskImageForKey:key data:data];
}

從磁盤中讀取圖片主要分成2個(gè)步驟,一是從磁盤中讀取出數(shù)據(jù),二是將數(shù)據(jù)轉(zhuǎn)化為圖片。

- (nullable NSData *)diskImageDataForKey:(nullable NSString *)key {
    if (!key) {
        return nil;
    }
    __block NSData *imageData = nil;
    //在ioQueue,阻塞當(dāng)前線程
    dispatch_sync(self.ioQueue, ^{
        imageData = [self diskImageDataBySearchingAllPathsForKey:key];
    });
    return imageData;
}

- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    //MD5字符串
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }
    //現(xiàn)在默認(rèn)路徑上找
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }
    //如果默認(rèn)路徑?jīng)]找到,則在其他路徑找,這些路徑可由開發(fā)者配置
    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
        if (imageData) {
            return imageData;
        }
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
        if (imageData) {
            return imageData;
        }
    }
    return nil;
}

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
    return [self diskImageForKey:key data:data options:0];
}

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
    if (data) {
        //圖片解碼
        UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
        //這里主要是進(jìn)行圖片放大、動(dòng)圖的操作
        image = [self scaledImageForKey:key image:image];
        if (self.config.shouldDecompressImages) {
            BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
            //解壓圖片
            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
        }
        return image;
    } else {
        return nil;
    }
}

除了同步獲取圖片的方法,該類還提供了異步獲取圖片的方法,其原理基本是一樣的,這里僅貼出接口:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;

他返回了一個(gè)NSOperation,開發(fā)者可以通過這個(gè)Operation中斷查找。

刪除圖片方法

刪除圖片的實(shí)現(xiàn)如下:

- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    [self removeImageForKey:key fromDisk:YES withCompletion:completion];
}

- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    if (key == nil) {
        return;
    }
    //從緩存刪除
    if (self.config.shouldCacheImagesInMemory) {
        [self.memCache removeObjectForKey:key];
    }

    if (fromDisk) {
        //在ioQueue中從磁盤中刪除
        dispatch_async(self.ioQueue, ^{
            [self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    } else if (completion){
        completion();
    }
    
}

可以看出,刪除圖片的操作還是比較簡(jiǎn)單的。

此外,還有清除內(nèi)存、清除磁盤的方法,代碼也比較簡(jiǎn)單,這里就不貼出來了。

刪除磁盤舊圖片功能實(shí)現(xiàn)

SDImageCache還可以定期刪除磁盤中的圖片,其實(shí)現(xiàn)方式是在程序進(jìn)入后臺(tái)或者程序結(jié)束時(shí),調(diào)用下面這個(gè)方法:

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
            //選擇是根據(jù)修改時(shí)間還是根據(jù)創(chuàng)建時(shí)間清除老文件
        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;

            case SDImageCacheConfigExpireTypeModificationDate:
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;

            default:
                break;
        }
        //清除文件,只需要知道文件是否是文件夾、時(shí)間以及占用大小3個(gè)信息
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
        //獲取文件的迭代
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        //得到過期的時(shí)間
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
            //排除錯(cuò)誤的文件以及文件夾
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }
             //如果文件過期,則添加到待刪除的數(shù)組中
            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            //如果沒有過期,則計(jì)算其占用大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        //如果這個(gè)時(shí)候總大小仍比配置的大小要大,則按照時(shí)間刪除文件,知道文件總大小小于配置大小的一半
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 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;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

總結(jié)

總的來說,緩存的邏輯主要復(fù)雜在磁盤的讀寫上,所有的磁盤操作都放在io線程上讀取。此外,在內(nèi)存上使用NSCache+NSMapTable而不是NSDictionary存儲(chǔ)圖片,也值得我們借鑒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容