SDWebImage源码详解 - 缓存

作者: 凌巅 | 来源:发表于2016-05-04 01:44 被阅读522次

    SDWebImage源码详解 - 缓存

    缓存的实现可以显著的减少网络流量的消耗,先将下载的图片缓存到本地,下次获取同一张图片的时候,可以直接在本地缓存中获取,而不用访问服务器重新获取图片,这样不仅可以减少网络流量的消耗,并且提升了用户体验(图片加载速度快)。SDWebImage的缓存由SDImageCache类来实现,这是一个单例类,该类负责处理内存缓存及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。此外还提供了若干属性和接口来配置和操作缓存对象。
    先来看看SDImageCache的头文件内容

    //定义三个枚举常量,以控制缓存的存储选项
    typedef NS_ENUM(NSInteger, SDImageCacheType) {
        //不使用缓存策略,从网络下载
        SDImageCacheTypeNone,
        //从磁盘中缓存中获取图片
        SDImageCacheTypeDisk,
        //从内存中获取图片
        SDImageCacheTypeMemory
    };
    
    //回调函数类型变量
    typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
    
    typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
    
    typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
    
    @interface SDImageCache : NSObject
    //是否在缓存之前解压图片,此项操作可以提升性能,但是会消耗较多的内存,默认是YES。注意:如果内存不足,可以置为NO
    @property (assign, nonatomic) BOOL shouldDecompressImages;
    
    //是否禁止iCloud备份,默认是YES
    @property (assign, nonatomic) BOOL shouldDisableiCloud;
    
    //是否启用内存缓存 默认是YES
    @property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
    
    //内存最大容量
    @property (assign, nonatomic) NSUInteger maxMemoryCost;
    
    //内存对象的最大数目
    @property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
    
    //磁盘缓存保留的最长时间
    @property (assign, nonatomic) NSInteger maxCacheAge;
    
    //磁盘缓存最大容量,以字节为单位
    @property (assign, nonatomic) NSUInteger maxCacheSize;
    
    //返回缓存对象的单例
    + (SDImageCache *)sharedImageCache;
    
    //以ns为缓存空间名字初始化缓存
    - (id)initWithNamespace:(NSString *)ns;
    
    //在directory目录下,以ns为缓存空间名字初始化缓存
    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
    
    //返回磁盘缓存空间的路径
    -(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
    
    //添加只读内存空间路径,一般用在图片已经下载置相应的缓存目录
    - (void)addReadOnlyCachePath:(NSString *)path;
    
    //以key为键值将图片image存储置缓存中
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key;
    
    //以key为键值将图片image存储置缓存中,toDisk控制是否写入磁盘缓存
    - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
    
    //以key为键值将图片image存储置缓存中,toDisk控制是否写入磁盘缓存,此外如果recalculate为YES或imageData有数据,则将imageData存储置磁盘缓存中
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
    
    //在内存或磁盘缓存中以key为键值查找图片缓存,如果找到则执行doneBlock回调
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
    
    //在内存缓存中查找图片缓存,并返回图片对象
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
    
    //在硬盘缓存中查找图片缓存,并返回图片对象
    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
    
    //在内存或硬盘缓存中删除指定key缓存
    - (void)removeImageForKey:(NSString *)key;
    
    //在内存或硬盘缓存中删除指定key缓存,完成后执行响应回调
    - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
    
    //在内存或硬盘缓存中删除指定key缓存,fromDisk控制是否删除磁盘缓存对象
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
    //在内存或硬盘缓存中删除指定key缓存,完成后执行响应回调,fromDisk控制是否删除磁盘缓存对象
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
    
    //清除内存缓存
    - (void)clearMemory;
    
    //清除磁盘缓存,完成后执行回调
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)clearDisk;
    
    //清除过期缓存,如果缓存容量超过限制,则清除部分缓存直至达到预期目标为止
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
    - (void)cleanDisk;
    
    //返回磁盘缓存的大小
    - (NSUInteger)getSize;
    //返回磁盘缓存对象的数目
    - (NSUInteger)getDiskCount;
    
    //异步计算磁盘缓存所需大小
    - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
    
    //异步查看磁盘缓存中是否存在指定key的图片,完成后执行回调
    - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
    - (BOOL)diskImageExistsWithKey:(NSString *)key;
    
    //返货指定路径path下的key对象的缓存路径
    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
    
    //返回默认路径下key对象的缓存路径
    - (NSString *)defaultCachePathForKey:(NSString *)key; 
    

    从头文件可以看出,SDWebImage的缓存对象提供了几个属性(缓存时间,缓存大小限制等)和若干函数来对缓存对象进行操作(获取、移除及清空图片)。对于这么多的函数,有些其实仅仅是调用而已,只需关注几个主要函数即可,稍后我们将会针对几个主要函数进行讲解。
    </br>
    SDWebImage缓存的主要实现分别采用了内存缓存和磁盘缓存,内存缓存使用NSCash对象来实现,NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类,NSCache类的详细用法,这里不过多介绍,以后有机会专门介绍。磁盘缓存则使用NSFileManager对象来实现。图片存储的位置是位于app的Cache文件夹下。另外,SDImageCache还定义了一个串行队列,来异步存储图片。接下我们就代码的执行流程来详细的看一下代码的实现:

    初始化缓存空间
    //获取内存对象的单例
    + (SDImageCache *)sharedImageCache {
        static dispatch_once_t once;
        static id instance;
        dispatch_once(&once, ^{
            instance = [self new];
        });
        return instance;
    }   
    

    ImageCache单例对象由函数new来初始换,而new函数默认调用init函数。

    - (id)init {
        return [self initWithNamespace:@"default"];
    }
    
    - (id)initWithNamespace:(NSString *)ns {
        //获取磁盘缓存的路径
        NSString *path = [self makeDiskCachePath:ns];
        return [self initWithNamespace:ns diskCacheDirectory:path];
    }
    
    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
        if ((self = [super init])) {
            NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
    
            //初始化PNG图片的签名数据
            kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
    
            // 创建IO 串行对垒
            _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    
            // 初始化最大缓存时间
            _maxCacheAge = kDefaultCacheMaxCacheAge;
    
            // 初始化内存缓存
            _memCache = [[AutoPurgeCache alloc] init];
            _memCache.name = fullNamespace;
    
            // 保存磁盘缓存的目录路径
            if (directory != nil) {
                _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
            } else {
                NSString *path = [self makeDiskCachePath:ns];
                _diskCachePath = path;
            }
    
            // 设置默认属性
            _shouldDecompressImages = YES;
            _shouldCacheImagesInMemory = YES;
            _shouldDisableiCloud = YES;
    
            dispatch_sync(_ioQueue, ^{
                _fileManager = [NSFileManager new];
            });
        
    #if TARGET_OS_IPHONE
            // 注册系统通知事件
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(clearMemory)
                                                         name:UIApplicationDidReceiveMemoryWarningNotification
                                                       object:nil];
    
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(cleanDisk)
                                                         name:UIApplicationWillTerminateNotification
                                                       object:nil];
    
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(backgroundCleanDisk)
                                                         name:UIApplicationDidEnterBackgroundNotification
                                                       object:nil];
    #endif
        }
    
        return self;
    }
    

    通过代码可以看出,ImageCache对象的初始化工作,分别创建了内存缓存空间和磁盘缓存空间,这里面有一个函数-(NSString *)makeDiskCachePath:(NSString*)fullNamespace木有出现,这个函数的主要作用就是返回app的缓存目录。

    -(NSString *)makeDiskCachePath:(NSString*)fullNamespace{
        //获取app的缓存文件夹
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        //返回缓存文件夹下以fullNamespace命名的路径
        return [paths[0] stringByAppendingPathComponent:fullNamespace];
    }
    

    保存图片

    虽然ImageCache对外提供了许多保存图片置缓存的函数,但是这么多函数都调用一个基础函数- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;,具体实现如下:

    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
        if (!image || !key) {
            return;
        }
        // 如果保存置内存缓存属性为YES,则将图片保留在内存缓存中
        if (self.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(image);
            [self.memCache setObject:image forKey:key cost:cost];
        }
        
        //如果需要保存在磁盘缓存中,则将写人磁盘缓存的队列放入创建的串行队列ioQueue中
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                NSData *data = imageData;
                
                //如果recalculate为YES或者data数据为空,但是image有数据,则对iamge图片做处理
                //如果recalculate为YES并且data数据非空,则直接对data数据进行保存
                if (image && (recalculate || !data)) {
    #if TARGET_OS_IPHONE
                     // 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。
                             // PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10
                    // 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。
                            //而当有图片数据时,我们检测其前缀,确定图片的类型
                    int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                    BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                      alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                      alphaInfo == kCGImageAlphaNoneSkipLast);
                    BOOL imageIsPng = hasAlpha;
    
                    // But if we have an image data, we will look at the preffix
                    if ([imageData length] >= [kPNGSignatureData length]) {
                        imageIsPng = ImageDataHasPNGPreffix(imageData);
                    }
    
                    if (imageIsPng) {
                        data = UIImagePNGRepresentation(image);
                    }
                    else {
                        data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                    }
    #else
                    data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    #endif
                }
                //创建缓存文件并存储图片
                if (data) {
                    //创建保留缓存文件的上层目录
                    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                    }
    
                    //以图片的URL做MD5转换后的文件名创建缓存文件
                    NSString *cachePathForKey = [self defaultCachePathForKey:key];
                    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
                    [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
    
                    // 是否启用iCloud云备份
                    if (self.shouldDisableiCloud) {
                        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                    }
                }
            });
        }
    }
    

    查询图片

    ImageCache对外提供了三个查询缓存图片的接口函数

    
    //在内存和磁盘缓存中查找key指定的图片
    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
    
    //在内存缓存中查找key指定的图片
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
    
    //先在内存缓存中查找,然后在磁盘缓存中查找key指定的图片
    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
    

    这里看一下第一个函数的实现,其他类似

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
        if (!doneBlock) {
            return nil;
        }
    
        if (!key) {
            doneBlock(nil, SDImageCacheTypeNone);
            return nil;
        }
    
        // 先在内存缓存中查找
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            doneBlock(image, SDImageCacheTypeMemory);
            return nil;
        }
        //如果内存缓存中没有找到,则去磁盘缓存中去查找- (UIImage *)diskImageForKey:(NSString *)key
        //在磁盘缓存中找到后,同时更新置内存缓存中
        //有回调则调用doneBlock回调
        NSOperation *operation = [NSOperation new];
        dispatch_async(self.ioQueue, ^{
            if (operation.isCancelled) {
                return;
            }
    
            @autoreleasepool {
                UIImage *diskImage = [self diskImageForKey:key];
                if (diskImage && self.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
    
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
    
        return operation;
    }
    

    移除图片

    ImageCache对外提供了四个删除缓存图片的函数,

    - (void)removeImageForKey:(NSString *)key;
    - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
    

    移除函数比较简单,也有一个基础函数- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;,这个函数比较简单,删除内存缓存,删除磁盘下的缓存文件,看一看代码就明白什么意思,这里就不过多说明

    清理缓存

    清理缓存图片的清理操作有内存清理和磁盘缓存清理,而磁盘缓存又可以分为完全清空和部分清理。完全清空操作是直接把缓存的文件夹移除,清空操作有以下三个方法:

    - (void)clearMemory;
    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
    - (void)clearDisk
    

    这三个函数比较简单,也不过多介绍
    接下来我们详细介绍一下部分清理,部分清空针对磁盘缓存,根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序排序,循环移除那些较早的文件,直到磁盘缓存的实际大小小于或等于我们设置的空间预设目标,这里设为最大缓存大小的一半。清理的操作在-cleanDiskWithCompletionBlock:方法中,其实现如下:

    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
            NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
            NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    
            // 该枚举器预先获取缓存文件的有用的属性,文件夹,修改时间,文件大小
            NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:resourceKeys
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
    
            NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
            NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
            NSUInteger currentCacheSize = 0;
    
            // 枚举缓存文件夹中所有文件,
                //该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
            NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
            for (NSURL *fileURL in fileEnumerator) {
                NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
    
                // 跳过文件夹
                if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                    continue;
                }
    
                // 将需要删除的文件,加入需要删除的数组urlsToDelete中
                NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
                if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                    [urlsToDelete addObject:fileURL];
                    continue;
                }
    
                //存储有效期内的文件大小,留作备用
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
                [cacheFiles setObject:resourceValues forKey:fileURL];
            }
            
            //删除过期缓存
            for (NSURL *fileURL in urlsToDelete) {
                [_fileManager removeItemAtURL:fileURL error:nil];
            }
    
            // 如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,首先删除最老的文件
            if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
                // 以设置的最大缓存大小的一半作为清理目标
                const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
    
                // 按照最后修改时间来排序剩下的缓存文件
                NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                    return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                }];
    
                // 循环删除文件,直到缓存总大小降到我们期望的大小
                for (NSURL *fileURL in sortedFiles) {
                    if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                        NSDictionary *resourceValues = cacheFiles[fileURL];
                        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                        currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
    
                        if (currentCacheSize < desiredCacheSize) {
                            break;
                        }
                    }
                }
            }
            //有回调则执行回调
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    

    到这里缓存的实现就讲解的差不多了,这里我们主要分析了SDWebImage的SDImageCache缓存类的相关操作,着重介绍了几个主要的操作,另外SDImageCache还提供了一些其他的辅助方法如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key指定的图片,具体的实现可以参照源码,实现都不怎么复杂。
    </br>
    下一节我们主要介绍一下异步下载器的实现。

    相关文章

      网友评论

      • 那片阳光已醉:大神,我只是用sd_setImageWithURL:placeholderImage: 这个方法来加载图片,但是他会造成内存警告怎么破啊?
        凌巅:@那片阳光已醉 啥意思?有没有详细的报错什么的?可否再详细一点

      本文标题:SDWebImage源码详解 - 缓存

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