美文网首页
SDWebImage源码解析二

SDWebImage源码解析二

作者: 凡凡_c009 | 来源:发表于2020-12-30 17:22 被阅读0次

    SDWebImage 5.0以后,项目中的代码文件非常多,这边对文件进行了一下简单区分,代码结构如下。


    结构图.png

    主要分为一下几块
    1、Render渲染
    2、Coder编码解码
    3、AnimatedImage动图加载
    4、Utils工具类
    5、Cache缓存
    6、Downloader下载
    7、Category相关类使用接口

    今天主要讲一下Cache这部分

    • 磁盘存储放在一个串行队列中
    • 创建内存存储和磁盘存储的Cache
    • 老版本升级图片转移
    • 退到后台或者杀掉APP,删除老的缓存
    ///单利
    @property (nonatomic, class, readonly, nonnull) SDImageCache *sharedImageCache
    
    ///实现
    - (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");
            
            // 创建串行队列
            _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");
            // 内存缓存
            _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;
    }
    

    图片存在到内存和磁盘

    • 内存存储
    • 磁盘存储,异步存储(串行队列)
      • 数据编码
      • 通过NSKeyedArchiver存储扩展信息,key和图片相同
    /// 图片存在到内存和磁盘
    - (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;
        }
        // 内存存储
        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;
                                }
                            }
                        }
                        // 数据编码
                        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();
            }
        }
    }
    

    查询图片

    • 首先从内存中查询
    • 对需要解码和动图处理
    • 从磁盘中查询
    • 内存中不存在,从磁盘解码出图片数据并存入磁盘中
    // 查询图片
    - (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;
        }
        
        // 首先从内存中查询
        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 内存缓存

    这个对象是继承自NSCache的。同时这个支持弱存储。
    NSCache是通过下面2个方法实现的

    - (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对数据进行存储和删除
    移除过期的磁盘数据

    /// NSFileManager的方法,可以获取到含有时间信息的URL的枚举数据
    - (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;
                    }
                }
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:SDWebImage源码解析二

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