SDWebImage5——框架内部部分细节
一、清空缓存
//1.清空缓存
//clear:直接删除缓存目录下面的文件,然后重新创建空的缓存文件
//clean:清除过期缓存,计算当前缓存的大小,和设置的最大缓存数量比较,如果超出那么会继续删除(按照文件了创建的先后顺序)
//过期时间:7天
[[SDWebImageManager sharedManager].imageCache cleanDisk];
[[SDWebImageManager sharedManager].imageCache clearMemory];
[[SDWebImageManager sharedManager].imageCache clearDisk];
二.取消当前所有的操作
[[SDWebImageManager sharedManager] cancelAll];
三. 最大并发数量
最大并发数量 6
设置的地方是 SDWebImageDownloader 的
- (id)init
方法中。
具体如下。
当我们在 UIImageView+WebCache 的下面这个方法中
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
}
使用到
SDWebImageManager.sharedManager
的时候,会调用 SDWebImageManager 的init
方法,如下
- (id)init {
if ((self = [super init])) {
_imageCache = [self createCache]; //初始化imageCache(单例)
_imageDownloader = [SDWebImageDownloader sharedDownloader]; //初始化imageDownloader(单例)
_failedURLs = [NSMutableSet new]; //初始化下载失败的URL(黑名单·空的集合)
_runningOperations = [NSMutableArray new]; //初始化当前正在处理的任务(图片下载操作·空的可变数组)
}
return self;
}
这里面会初始化 SDWebImageDownloader 的属性 imageDownloader ,这个时候回调用到 SDWebImageDownloader 的 init方法,如下
//异步下载器初始化方法
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class]; //获得类型
_shouldDecompressImages = YES; //是否解码,默认为YES(以空间换取时间)
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder; //下载任务的执行方式:默认为先进先出
_downloadQueue = [NSOperationQueue new]; //创建下载队列:非主队列(在该队列中的任务在子线程中异步执行)
_downloadQueue.maxConcurrentOperationCount = 6; //设置下载队列的最大并发数:默认为6
_URLCallbacks = [NSMutableDictionary new]; //初始化URLCallbacks字典
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; //处理请求头
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
//创建栅栏函数添加的队列:自己创建的并发队列
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0; //设置下载超时为15秒
}
return self;
}
我们可以看到 最大并发数量 = 6
四.缓存文件的保存名称如何处理?
拿到图片的URL路径,对该路径进行MD5加密
方法是 SDImageCache中的
- (NSString *)cachedFileNameForKey:(NSString *)key
下面我们来看一下。
由前面的文章我们知道,我们的最终会调用到 SDWebImageManager 的 downloadImageWithURL
的方法。在这个方法的成功的回调里面,会进行磁盘存储。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
............
//是否要进行磁盘缓存?
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
//如果下载策略为SDWebImageRefreshCached且该图片缓存中存在且未下载下来,那么什么都不做
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//否则,如果下载图片存在且(不是可动画图片数组||下载策略为SDWebImageTransformAnimatedImage&&transformDownloadedImage方法可用)
//开子线程处理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//在下载后立即将图像转换,并进行磁盘和内存缓存
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
#warning 2
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
//在主线程中回调completedBlock
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
............
}
上面会的代码中会进行内存和磁盘缓存,具体的调用是
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
我们进入到这个方法中看一下,好的,那么我们接下来就到了 SDImageCache 的如下代码中
//使用指定的键将图像保存到内存和可选的磁盘缓存
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
//如果图片或对应的key为空,那么就直接返回
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) {
//异步函数+串行队列:开子线程异步处理block中的任务
dispatch_async(self.ioQueue, ^{
//拿到服务器返回的图片二进制数据
NSData *data = imageData;
//如果图片存在且(直接使用imageData||imageData为空)
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// We need to determine if the image is a PNG or a JPEG
// PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
// The first eight bytes of a PNG file always contain the following (decimal) values:
// 137 80 78 71 13 10 26 10
// If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
// and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
//获得该图片的alpha信息
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
//判断该图片是否是PNG图片
BOOL imageIsPng = hasAlpha;
// But if we have an image data, we will look at the preffix
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
//如果判定是PNG图片,那么把图片转变为NSData压缩
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
//否则采用JPEG的方式
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
if (data) {
//确定_diskCachePath路径是否有效,如果无效则创建
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
// 根据key获得缓存路径
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
//把路径转换为NSURL类型
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//使用文件管理者在缓存路径创建文件,并设置数据
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// disable iCloud backup
//如果禁用了iCloud备份
if (self.shouldDisableiCloud) {
//标记沙盒中不备份文件(标记该文件不备份)
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
上面的代码中,我们关注下面的代码
// 根据key获得缓存路径
NSString *cachePathForKey = [self defaultCachePathForKey:key];
接下来我们看到 defaultCachePathForKey 方法
//获得指定 key 的默认缓存路径
- (NSString *)defaultCachePathForKey:(NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
然后我们再到 - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path
//获得指定 key 对应的缓存路径
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
//获得缓存文件的名称
NSString *filename = [self cachedFileNameForKey:key];
//返回拼接后的全路径
return [path stringByAppendingPathComponent:filename];
}
发现它调用了 - (NSString *)cachedFileNameForKey:(NSString *)key
方法,如下
//对key(通常为URL)进行MD5加密,加密后的密文作为图片的名称
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
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;
}
现在我们看到了,它是通过 MD5 来处理URL地址的,然后用作文件名。
五、该框架内部对内存警告的处理方式?
内部通过监听通知的方式清理缓存
如下,
既然是内存警告的处理,那么我们想到就是缓存的问题,我们来到 SDImageCache 中
如下
//初始化
- (id)init
{
self = [super init];
if (self) {
//监听到UIApplicationDidReceiveMemoryWarningNotification(应用程序发生内存警告)通知后,调用removeAllObjects方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
发生内存警告的时候,清除内存中的所有缓存对象。
除此之外,还有下面的方法中也有
//使用指定的命名空间实例化一个新的缓存存储和目录
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
............
#if TARGET_OS_IPHONE
// Subscribe to app events
//监听应用程序通知
//当监听到UIApplicationDidReceiveMemoryWarningNotification(系统级内存警告)调用clearMemory方法
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
//当监听到UIApplicationWillTerminateNotification(程序将终止)调用cleanDisk方法
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
//当监听到UIApplicationDidEnterBackgroundNotification(进入后台),调用backgroundCleanDisk方法
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
六、该框架进行缓存处理的方式
既然是看缓存的处理,那么我们还是直接看 SDImageCache 类,发现它是继承自 NSCache的,
那么也就是说它是用 NSCache 来处理缓存的。
七.如何判断图片的类型
在判断图片类型的时候,只匹配第一个字节。这个的处理在 NSData+ImageContentType中
//
// Created by Fabrice Aneche on 06/01/14.
// Copyright (c) 2014 Dailymotion. All rights reserved.
//
#import "NSData+ImageContentType.h"
@implementation NSData (ImageContentType)
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
uint8_t c;
//获得传入的图片二进制数据的第一个字节
[data getBytes:&c length:1];
//在判断图片类型的时候,只匹配第一个字节
switch (c) {
case 0xFF:
return @"image/jpeg";
case 0x89:
return @"image/png";
case 0x47:
return @"image/gif";
case 0x49:
case 0x4D:
return @"image/tiff";
case 0x52:
//WEBP :是一种同时提供了有损压缩与无损压缩的图片文件格式
// R as RIFF for WEBP
if ([data length] < 12) {
return nil;
}
//获取前12个字节
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
//如果以『RIFF』开头,且以『WEBP』结束,那么就认为该图片是Webp类型的
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return @"image/webp";
}
//否则返回nil
return nil;
}
return nil;
}
@end
@implementation NSData (ImageContentTypeDeprecated)
+ (NSString *)contentTypeForImageData:(NSData *)data {
return [self sd_contentTypeForImageData:data];
}
@end
八、队列中任务的处理方式
任务的处理方式 FIFO。
这个体现在,SDWebImageDownloader 的 init
方法中
//异步下载器初始化方法
- (id)init {
····
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder; //下载任务的执行方式:默认为先进先出
····
return self;
}
我们可以看一下,SDWebImageDownloaderExecutionOrder 枚举
//下载操作的执行方式
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
/*
* 默认值,所有下载操作将按照队列的先进先出方式执行
*/
SDWebImageDownloaderFIFOExecutionOrder,
/*
* 所有下载操作将按照堆栈的后进先出方式执行
*/
SDWebImageDownloaderLIFOExecutionOrder
};
九.如何下载图片的?
发送网络请求下载图片,使用NSURLConnection
具体体现,我们来到 SDWebImageDownloaderOperation 类中,看到它的 “start“`方法
//核心方法:在该方法中处理图片下载操作
- (void)start {
....
//创建NSURLConnection对象,并设置代理(没有马上发送请求)
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
....
[self.connection start]; //发送网络请求
....
}
十.请求超时的时间
请求超时的时间,是 15s。
其实这个问题我们在 iOS学习笔记-128.SDWebImage4——框架内部调用简单分析 中就说过啦。
下面我们再来看一下,来到 SDWebImageDownloader 的下面这个方法中,我们会发现,超时时间就是 15s.
//核心方法:下载图片的操作
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
..........
//处理下载超时,如果没有设置过则初始化为15秒
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
..........
}