SDWebImage *底层探究 (二)

图片加载:

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] 
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
# 其实是通过 SDWebImageManager类进行协调,调用 SDImageCache与 SDWebImageDownloader来实现图片的缓存查询与网络下载的。

1. SDImageCache

该类维护了一个内存缓存与一个可选的磁盘缓存. 同时, 磁盘缓存的写操作是异步的, 所以他不会对UI造成不必要的影响.

*每次查询图片时, 首先会根据图片的URL对应的key值检测内存中是否有对应的图片:
@ 如果有则直接返回;
@ 如果没有则在ioQueue中去磁盘中查找;
其key是根据URL生成的MD5值, 找到图片缓存在内存中, 然后把图片返回.

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
     if (!doneBlock) { 
            return nil; 
     }
     if (!key) { 
            doneBlock(nil, SDImageCacheTypeNone); 
            return nil; 
     } 

    // 首先检查内存缓存(查询是同步的),如果查找到,则直接回调 doneBlock 并返回 
    UIImage *image = [self imageFromMemoryCacheForKey:key]; 
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory); 
        return nil;
     } 

    NSOperation *operation = [NSOperation new]; 
    dispatch_async(self.ioQueue, ^{ 
        if (operation.isCancelled) { 
            return; 
        } 
    // 创建自动释放池,内存及时释放 
    @autoreleasepool { 
        // 检查磁盘缓存(查询是异步的),如果查找到,则将其放到内存缓存,并调用 doneBlock 回调
        UIImage *diskImage = [self diskImageForKey:key]; 
        if (diskImage) { 
            NSUInteger cost = SDCacheCostForImage(diskImage); 
            // 缓存至内存(NSCache)中 
            [self.memCache setObject:diskImage forKey:key cost:cost];
        } 
        // 返回主线程设置图片 
        dispatch_async(dispatch_get_main_queue(), ^{ 
            doneBlock(diskImage, SDImageCacheTypeDisk); 
        });
     }
   }); 
  return operation;
}

2. NSCache

NSCache 是苹果官方提供的缓存类,用法与 NSMutableDictionary 的用法很相似,在 SDWebImage 和 AFNetworking 中,使用它来管理缓存。同样是以 key-value 的形式进行存储,那么 NSCache 与 NSMutableDictionary 等集合类的区别或者说优势又是哪些呢?

  • NSCache 类结合了各种自动删除策略,以确保不会占用过多的系统内存。如果其它应用需要内存时,系统自动执行这些策略。当调用这些策略时,会从缓存中删除一些对象,以最大限度减少内存的占用
  • NSCache 是线程安全的,我们可以在不同的线程中添加、删除和查询缓存中的对象,而不需要锁定缓存区域
  • 不像 NSMutableDictionary 对象,NSCache 对象并不会拷贝键(key),而是会强引用它

要点

  1. 在开发者自己编写加锁代码的前提下, 多个线程便可以同时访问NSCache
  2. NSCache对象不拷贝键的原因在于: 很多时候, 键都是由不支持拷贝操作的对象来充当的. 所以说, 在不支持拷贝操作的情况下, 该类用起来比字典更方便.
  3. 可以给NSCache对象设置上限, 用以限制缓存中的对象总个数, 而这些尺度则定义了缓存删减中对象的时间. 但是绝对不要把这些尺度当成靠山, 他们仅对于NSCache起指导作用.
  4. 将NSPurgeableData与NSCache搭配使用, 可实现自动清除数据的功能, 也就是说, 当NSPurgeableData对象所占内存为系统所丢弃时, 该对象自身也会从缓存中移除.
  5. 如果缓存使用得当, 那么应用程序的响应速度就能提高. 只有那种(重新计算起来哼费时的)数据, 才值得放入缓存, 比如那些需要从网络获取或者从磁盘读取的数据.
  6. 内存查询是同步, 磁盘查询是异步.

3. 磁盘

磁盘缓存的处理则是使用NSFileManager对象来实现的. 默认以com.hackemist.SDWebImageCache.default为磁盘的缓存命名空间, 程序运行后, 可以在程序的文件夹Library/Caches/default/com.hackemist.SDWebImageCache.default下看到一些缓存文件. 另外, SDImageCache还定义了一个串行队列, 来异存储图片.

在磁盘查询的时候, 会在后台将NSData转场UIImage, 并完成相关的解码工作:

- (UIImage *)diskImageForKey:(NSString *)key { 
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; 
    if (data) {   
        UIImage *image = [UIImage sd_imageWithData:data]; 
        image = [self scaledImageForKey:key image:image]; 
        if (self.shouldDecompressImages) {
             image = [UIImage decodedImageWithImage:image]; 
        }
        return image;
    } else {
       return nil;
    }
}

4. 存储图片

当下载玩图片后, 会先将图片保存到NSCache中, 并把图片像素大小作为该对象的cost值, 同时如果需要保存到硬盘, 会先判断图片的格式, PNG 和JPEG, 并保存对应的NSData到缓存路径中, 文件名为URL 的MD5值:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk { 
    ...
    // 内存缓存,将其存入 NSCache 中,同时传入图片的消耗值,cost 为像素值(当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象) 
    [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale]; 
    if (toDisk) { 
         // 如果确定需要磁盘缓存,则将缓存操作作为一个任务放入 ioQueue 中 
         dispatch_async(self.ioQueue, ^{ 
            // 构建一个 data,用来存储到 disk 中,默认值为 imageData 
            NSData *data = imageData; 
            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 以避免丢失透明度。而当有图片数据时,我们检测其前缀,确定图片的类型 
                 BOOL imageIsPng = YES; 
                 if ([imageData length] >= [kPNGSignatureData length]) { 
                     imageIsPng = ImageDataHasPNGPreffix(imageData); 
                 }
                // 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 将其转化为 NSData,否则按照 JPEG 格式转化,并且压缩质量为 1,即无压缩 
                if (imageIsPng) { 
                     data = UIImagePNGRepresentation(image); 
                } else { 
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0); 
                }
#else
               data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif 
        } 
        // 创建缓存文件并存储图片(使用 fileManager) 
        if (data) {
             if (![_fileManager fileExistsAtPath:_diskCachePath]) { 
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; 
              } 
            // 保存 data 到指定的路径中
            [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil]; 
        } 
    }); 
  }
}

5. 清理图片

SDImageCache 会在系统发出低内存警告时释放内存,并且在程序进入 UIApplicationWillTerminateNotification 时,清理磁盘缓存,清理磁盘的机制是:

  1. 删除过期的图片,默认 7 天过期,可以通过 maxCacheAge 修改过期天数。

  2. 如果缓存的数据大小超过设置的最大缓存 maxCacheSize,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间,可以通过修改 maxCacheSize 来改变最大缓存大小。

- (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; } 
        // 移除早于有效期的老文件 
        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(); }); 
  }
 });
}

http://itangqi.me/2016/03/23/the-notes-of-learning-sdwebimage-three/
http://itangqi.me/2016/03/24/the-notes-of-learning-sdwebimage-four/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,627评论 6 517
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,180评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 169,346评论 0 362
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,097评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,100评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,696评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,165评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,108评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,646评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,709评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,861评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,527评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,196评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,698评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,804评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,287评论 3 379
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,860评论 2 361

推荐阅读更多精彩内容