• SDWebImage缓存机制


    存 取 删 路径

    1.1 存

    是在storeImage这个方法里:

    将图片储存到内存和硬盘上

    -(void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // if memory cache is enabled
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            // 如果image存在,但是需要重新计算(recalculate)或者data为空
            // 那就要根据image重新生成新的data
            // 不过要是连image也为空的话,那就别存了
            if (image && (recalculate || !data)) {
    #if TARGET_OS_IPHONE
                // 我们需要判断image是PNG还是JPEG
                // PNG的图片很容易检测出来,因为它们有一个特定的标示 (http://www.w3.org/TR/PNG-Structure.html)
                // PNG图片的前8个字节不许符合下面这些值(十进制表示)
                // 137 80 78 71 13 10 26 10
                
                // 如果imageData为空l (举个例子,比如image在下载后需要transform,那么就imageData就会为空)
                // 并且image有一个alpha通道, 我们将该image看做PNG以避免透明度(alpha)的丢失(因为JPEG没有透明色)
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);// 获取image中的透明信息
                // 该image中确实有透明信息,就认为image为PNG
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;
    
                // 但是如果我们已经有了imageData,我们就可以直接根据data中前几个字节判断是不是PNG
                if ([imageData length] >= [kPNGSignatureData length]) {
                    // ImageDataHasPNGPreffix就是为了判断imageData前8个字节是不是符合PNG标志
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
    
                // 如果image是PNG格式,就是用UIImagePNGRepresentation将其转化为NSData,否则按照JPEG格式转化,并且压缩质量为1,即无压缩
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
    #else
                // 当然,如果不是在iPhone平台上,就使用下面这个方法。不过不在我们研究范围之内
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    #endif
            }
    
            // 获取到需要存储的data后,下面就要用fileManager进行存储了
            if (data) {
                // 首先判断disk cache的文件路径是否存在,不存在的话就创建一个
                // disk cache的文件路径是存储在_diskCachePath中的
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }
    
                // 根据image的key(一般情况下理解为image的url)组合成最终的文件路径
                // 上面那个生成的文件路径只是一个文件目录,就跟/cache/images/img1.png和cache/images/的区别一样
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                // 这个url可不是网络端的url,而是file在系统路径下的url
                // 比如/foo/bar/baz --------> file:///foo/bar/baz
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    
                // 根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
    
                // disable iCloud backup
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
    }
    

    1.2 取

    内存缓存使用NSCache的objectForKey取数据:

    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    	return [self.memCache objectForKey:key];
    }
    

    磁盘取数据 不断用 dataWithContentsOfFile来试数据是否在key对应的路径中

    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
    
    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }
    
    // Second check the disk cache...
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }
    
    return diskImage;
    }
    

    1.3 删

    1. removeImageForKeyfromDisk:withCompletion: // 异步地将image从缓存(内存缓存以及可选的磁盘缓存)中移除
    2. clearMemory // 清楚内存缓存上的所有image
    3. clearDisk // 清除磁盘缓存上的所有image
    4. cleanDisk // 清除磁盘缓存上过期的image

    看其中最长的一个:

    // 实现了一个简单的缓存清除策略:清除修改时间最早的file
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        // 这两个变量主要是为了下面生成NSDirectoryEnumerator准备的
        // 一个是记录遍历的文件目录,一个是记录遍历需要预先获取文件的哪些属性
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    
        // 递归地遍历diskCachePath这个文件夹中的所有目录,此处不是直接使用diskCachePath,而是使用其生成的NSURL
        // 此处使用includingPropertiesForKeys:resourceKeys,这样每个file的resourceKeys对应的属性也会在遍历时预先获取到
        // NSDirectoryEnumerationSkipsHiddenFiles表示不遍历隐藏文件
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        // 获取文件的过期时间,SDWebImage中默认是一个星期
        // 不过这里虽然称*expirationDate为过期时间,但是实质上并不是这样。
        // 其实是这样的,比如在2015/12/12/00:00:00最后一次修改文件,对应的过期时间应该是
        // 2015/12/19/00:00:00,不过现在时间是2015/12/27/00:00:00,我先将当前时间减去1个星期,得到
        // 2015/12/20/00:00:00,这个时间才是我们函数中的expirationDate。
        // 用这个expirationDate和最后一次修改时间modificationDate比较看谁更晚就行。
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        // 用来存储对应文件的一些属性,比如文件所需磁盘空间
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        // 记录当前已经使用的磁盘缓存大小
        NSUInteger currentCacheSize = 0;
    
        // 在缓存的目录开始遍历文件.  此次遍历有两个目的:
        //
        //  1. 移除过期的文件
        //  2. 同时存储每个文件的属性(比如该file是否是文件夹、该file所需磁盘大小,修改时间)
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
    
            // 当前扫描的是目录,就跳过
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }
    
            // 移除过期文件
            // 这里判断过期的方式:对比文件的最后一次修改日期和expirationDate谁更晚,如果expirationDate更晚,就认为该文件已经过期,具体解释见上面
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
    
            // 计算当前已经使用的cache大小,
            // 并将对应file的属性存到cacheFiles中
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            // 根据需要移除文件的url来移除对应file
            [_fileManager removeItemAtURL:fileURL error:nil];
        }
    
        // 如果我们当前cache的大小已经超过了允许配置的缓存大小,那就删除已经缓存的文件。
        // 删除策略就是,首先删除修改时间更早的缓存文件
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 直接将当前cache大小降到允许最大的cache大小的一般
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
    
            // 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在前的规则排序
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];
    
            // 每次删除file后,就计算此时的cache的大小
            // 如果此时的cache大小已经降到期望的大小了,就停止删除文件了
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    // 获取该文件对应的属性
                    NSDictionary *resourceValues = cacheFiles[fileURL];
        // 根据resourceValues获取该文件所需磁盘空间大小
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        // 计算当前cache大小
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
    
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        // 如果有completionBlock,就在主线程中调用
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
    }
    

    1.4 图片储存路径

    // 简单封装了cachePathForKey:inPath
    - (NSString *)defaultCachePathForKey:(NSString *)key {
    	return [self cachePathForKey:key inPath:self.diskCachePath];
    }
    
    // cachePathForKey:inPath
    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
    // 根据传入的key创建最终要存储时的文件名
    NSString *filename = [self cachedFileNameForKey:key];
    // 将存储的文件路径和文件名绑定在一起,作为最终的存储路径
    return [path stringByAppendingPathComponent:filename];
    }
    
    // cachedFileNameForKey:
    - (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    // 使用了MD5进行加密处理
    // 开辟一个16字节(128位:md5加密出来就是128bit)的空间
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    // 官方封装好的加密方法
    // 把str字符串转换成了32位的16进制数列(这个过程不可逆转) 存储到了r这个空间中
    CC_MD5(str, (CC_LONG)strlen(str), r);
    // 最终生成的文件名就是 "md5码"+".文件类型"
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
    
    return filename;
    }
  • 相关阅读:
    linux下配置java环境及问题
    Chrome工具使用
    Ibatis的resultMap和查询数据的对应关系
    spring mvc接收数组
    PowderDesign的使用
    Android Runtime Stats
    [原创]ASM动态修改JAVA函数之函数字节码初探
    [原创]WB Android客户端架构总结:发WB工作队列设计
    [原创]Android Studio的Instant Run(即时安装)原理分析和源码浅析
    [原创]Android系统中常用JAVA类源码浅析之HashMap
  • 原文地址:https://www.cnblogs.com/sunyanyan/p/5417706.html
Copyright © 2020-2023  润新知