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