美文网首页IOS开发知识点
SDWebImage组成与缓存策略

SDWebImage组成与缓存策略

作者: MilesQL | 来源:发表于2020-06-28 00:10 被阅读0次

SDWebImage是iOS开发中最常使用的网络图片加载库,这个库包含了比较完善的网络图片加载、缓存与性能优化的功能集成;以下这张图包含了这个库的功能模块组成;可以看到为了实现网络图片的加载与本地缓存,SDWebImage拆分了不同的模块自来实现对应的功能子集;

组成结构.png

上图红线框中选中的部分包括下载、缓存与图片编解码处理,属于框架的核心部分;其他部分的功能主要用于对核心部分做支撑和功能扩展,以下是对核心模块的功能作用说明:

  • Manager:网络图片处理的调度器,实际的图片下载与缓存是由这里组装和发起的,同时这个类也是UI层Category扩展功能的背后支撑类;
  • Downloader:负责管理图片的网络下载任务;内部通过一个NSOperationQueue下载队列实现
  • Cache:负责图片的内存缓存和本地磁盘缓存的处理
  • Decoder:负责不同格式的图片数据与NSData之间的编解码处理,Cache中存储的数据都是NSData格式的

其他部分包括“Private”、UI控件的Category扩展以及动态图片的处理部分等,主要提供了框架内部使用到的一些工具类方法和控件使用的快捷调用封装等,方便开发者调用;SDWebImage本身的代码不是很难,但是其模块的拆分和代码设计非常优秀,整个库的代码实现比较优雅

这篇文章主要是对‘Cache’部分的理解,也就是‘SDWebImage’对网络图片的缓存处理方案;以下是SDWebImage关于缓存部分的UML设计类图,通过这张图我们可以知道缓存部分的内部设计结构与类组成,以及基本功能方法:

SDWebImageCacheClassDiagram.png

可以看到缓存部分类组成很清晰,分别包括以下功能类:

  • SDImageCache:负责处理图片的缓存调度,图片的数据缓存处理是从这个类中发起的
  • SDImageCacheConfig:负责缓存全局配置,可以通过这个类来设置磁盘缓存的过期时间,以及可以缓存的最大字节数等
  • SDMemoryCache:处理内存缓存
  • SDDiskCache:处理磁盘缓存
  • SDImageCacheDefine:这个类中定义了SDImageCache协议,和一个NSData图片解码的方法入口;我们可以定义自己的缓存处理策略类 来替换掉框架默认实现的SDImageCache缓存处理类,以改变缓存处理的具体逻辑,只要自定义的类遵从并实现SDImageCache的协议即可
  • SDImageCachesManager:当需要处理多个Cache对象时,可以通过这个类的功能来完成多缓存之间的基本操作调度,大多数时候只需要一个Cache对象(内部包括一个内存缓存对象和一个磁盘缓存对象)

SDWebImage关于Cache部分的处理主要包括,缓存的增、删、查、判断有无、及缓存的清理这几个部分;

这里涉及一个比较重要的概念,就是关联到每个缓存数据的key值,框架内部会使用传进来的图片名称作为原始数据,通过CC_MD5算法生成一个图片的唯一标识符,这个唯一标识符会作为缓存key值与具体的某一条缓存数据关联,通过这个key值可以执行增删查等操作;因此需要确保每一张缓存的图片名称是唯一的,以保证图片缓存数据不会被相同的key覆盖;在加载的网络图片时,默认会把图片URL作为图片的名称得到唯一标识符;

以下在深入看一下内存缓存部分磁盘缓存部分、和缓存的清理策略在框架内部的具体实现策略

内存缓存部分

SDMemoryCache类用来实现默认的内存缓存方式,这个类继承自NSCache,并遵循SDMemoryCache协议,实现了对应的协议方法;框架上声明了SDMemoryCache协议,并没有直接写死具体的缓存策略,这么做的目的是未来可以更方便的对缓存策略做不同的实现扩展,或者让用户可以实现自定义的缓存处理策略(只需要实现自己的类遵循SDMemoryCache协议,并实现具体的协议方法来处理自己的缓存逻辑即可);这体现了面向对象编程所倡导的面向接口编程而不是面向实现编程的编程思想;同理在磁盘缓存中也有类似的实践。

SDMemoryCache内部主要通过调用父类NSCache的方法来实现内存缓存数据的读与写,这个类中的缓存处理方法是在同步线程中完成的,会阻塞当前线程;这是由于内存缓存的访问本身并不耗时 即使同步访问也不会带来明显的性能问题;这个类的私有属性主要包括以下几个:

@property (nonatomic, strong, nullable) SDImageCacheConfig *config;
#if SD_UIKIT
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
@end

其中weakCache属性,会在内部对缓存数据做一个弱引用,其目的是在内存缓存被释放时(如应用退后台等),如果当前有UI控件类似 UIImageView等页面显示的对象还持有了图片数据时,在应用返回前台时,可以根据这个弱引用key缓存的数据重新缓存数据到内存中;

weakCacheLock属性,是一个GCD锁,保证了操作weakCache属性的线程安全性;

磁盘缓存部分

SDDiskCache类用来实现默认的磁盘数据缓存方式,其内部主要通过一个NSFileManager对象来实现磁盘文件对象的读写操作;我们同样可以不使用框架内部默认的实现方式,通过实现自己的类并遵循SDMemoryCache协议,来实现自定义的磁盘缓存策略替代方案;

SDDiskCache内部主要通过NSFileManager来操作NSData缓存数据的读写与删除方法,由于磁盘读写相对耗时,且磁盘的读写操作需要做到线程安全;因此这个类中方法调用都是在一个dispatch_queue_tSerial类型的 IO队列里完成的,以确保多线程下的数据读写安全,同时保证不阻塞主线程的正常流程,避免带来性能问题;

这个类内部主要实现的是关于NSFileManager文件的读写操作,在接收缓存数据时,如果传入的是UIImage类型的数据,则会通过Decoder模块先对图片做编码操作,转化成NSData后再完成本地的文件存储调用;这里会通过对传递的key调用CC_MD5方法来组合生成一个唯一文件名,关联到这个缓存对象;

以下可以看下这个类中关于读写缓存数据的API原型:

/**
 Returns the data associated with a given key.
 This method may blocks the calling thread until file read finished
 */
- (nullable NSData *)dataForKey:(nonnull NSString *)key;

/**
 Sets the value of the specified key in the cache.
 This method may blocks the calling thread until file write finished.
 
 @param data The data to be stored in the cache.
 */
- (void)setData:(nullable NSData *)data forKey:(nonnull NSString *)key;

/**
 Removes the value of the specified key in the cache.
 This method may blocks the calling thread until file delete finished. 
 */
- (void)removeDataForKey:(nonnull NSString *)key;

/**
 Empties the cache.
 This method may blocks the calling thread until file delete finished.
 */
- (void)removeAllData;

/**
 Removes the expired data from the cache. You can choose the data to remove base on `ageLimit`, `countLimit` and `sizeLimit` options.
 */
- (void)removeExpiredData;

@end

缓存清理部分

首先SDWebImage允许我们根据指定的key值,来清理特定的换粗图片数据;只需要调用对应的API传入key值就可以做到;以下部分是关于全局的缓存清理策略与方法的说明

SDWebImage默认的缓存清理周期是一星期,当然框架上允许我们自己设置这个过期时间,通过设置SDImageCacheConfig缓存配置类的maxDiskAge属性即可;具体的缓存清理策略分为两种,如以下两个枚举值所示:

缓存清理策略.png

第一种是按图片的最后访问时间计时,超过设置的过期时间后被清理;
第二种是按图片的最后修改时间计时,例如重新下载设置了图片,已最后一次修改过图片的时间节点来计时,超过设置的过期时间后被清理

清理的动作会在应用程序退出时(applicationWillTerminate方法被调用) 执行一次;默认情况下应用退出到后台时(并未退出),也会执行一次检测清理的操作,不过这个检测开关可以有开发者来设置,默认是开启的;

以下是内存缓存磁盘缓存的缓存清理执行方法,通过这两个方法我们能更清晰的看出来缓存是如果被清理掉的:

///内存缓存清理方法
- (void)removeAllObjects {
    [super removeAllObjects];  //调用父类NSCache的方法,可以直接清理完内存缓存

    //判断是否有缓存的弱引用存在,如果有需要把弱引用对象一起清理掉
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    // Manually remove should also remove weak cache
    SD_LOCK(self.weakCacheLock);
    [self.weakCache removeAllObjects];
    SD_UNLOCK(self.weakCacheLock);
}
@end

可以看到内容的缓存清理方法实际上非常简单,只需要调用父类NSCache的 removeAllObjects方法就可以完成点缓存的清理任务;关于self.weakCacheLock对象的作用在内存缓存部分已做过说明,这里因为内存缓存已不再需要,需要一并把这部分弱引用一起清理掉;

///磁盘缓存清理方法

- (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;
            
        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;
                }
            }
        }
    }
}

单个数据的磁盘缓存清理非常简单,只要根据对应的key检索到文件,并判断文件存在,直接调用NSFileManagerremoveItemAtPath方法即可完成清理;

以上这个方法的是关于如何清理过期的图片缓存数据的,总结起来过程如下:

  • 获取出所有的缓存对象,首先清理掉过期的缓存对象;
  • 计算出未过期的缓存大小总和,并与最大的缓存空间的 1/2 作对比;
  • 按时间最久未被访问(或修改)的顺序,对缓存数据做一个排序;
  • 根据排序结果继续清理 时间最久远的缓存图片数据,直到剩余的缓存大小 小于最大控件的1/2时,停止清理;

以上就是SDWebImage这个网络库关于缓存部分的大致实现原理;通过这篇文档我们能大概了解到SDWebImage大致组成结构和其内部的缓存处理方法,这个图片库本身的代码并不复杂,最优秀的地方其实应该体现在其代码的结构设计上;

相关文章

网友评论

    本文标题:SDWebImage组成与缓存策略

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