前言:
IOS SDWebImage 2.X源码阅读(一)
IOS SDWebImage 2.X源码阅读(二)
IOS SDWebImage 2.X源码阅读(三)
IOS SDWebImage 2.X源码阅读(四)
(6)上一篇讲了下载图片的相关操作,下面我们看下在图片下载完层之后,回调相关block
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
// barrierQueue是为了保证同一时刻只有一个线程对URLCallbacks进行操作
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
//有用户自定义block的话,就回调
if (callback) callback(receivedSize, expectedSize);
});
}
}
这部分是进度条相关的block,
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
dispatch_sync(queue,block),同步,会阻塞当前线程,等到其后面的block执行完成之后,才继续执行其后的代码,也就保证了同一时刻只有一个线程能对URLCallbacks进行操作,跟在addProgressCallback方法中用dispatch_barrier_sync对URLCallbacks操作
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
AppLog(@"图片数据下载完成")
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
//同步barrier任务将下载完成的url等操作从URLCallbacks从移除
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
AppLog(@"completed callbacksForURL--->%@",callbacksForURL);
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
//将刚下载完成的block
if (callback) callback(image, data, error, finished);
}
}
1、在SDWebImageDownloaderCompletedBlock中,下载图片是多线程异步下载的,但是在每个线程中图片下载完成后,使用的是dispatch_barrier_sync(queue,block),保证前面的操作完成之后,才能执行dispatch_barrier_sync中的block,dispatch_barrier_sync中的block执行完成之后,才能继续执行其后的代码。
2、该url下载操作完成之后,需要将该url对应的相关操作都移除 :[sself.URLCallbacks removeObjectForKey:url];
3、然后遍历,将对应下载成功的block,回调
取消的功能类似
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
我们看下如下方法相关的回调
[self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished);
//operation不存在,或者取消,啥都不处理,因为如果我们调用completedBlock,可能会在另外的地方我们也调用了,或覆盖数据
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
}
else if (error) {//下载失败
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
下载完成的其他情况的相关block
AppLog(@"SDWebImageManager下载成功回调completedBlock");
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
如果设置了SDWebImageRetryFailed属性,需要将url从failedURLs中移除,以便可以重新下载
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
AppLog(@"是否缓存在磁盘:cacheOnDisk = %d",cacheOnDisk);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block图像刷新命中NSURLCache缓存,不要调用完成块
}
是否需要缓存在磁盘缓存中
若是图片命中NSURLCache缓存,不需要调用complete block
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
AppLog(@"gif图像相关操作???");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
1、有下载的图像downloadedImage
2、(downloadedImage不是gif动图 || (设置了SDWebImageTransformAnimatedImage,要给它设置动画))
3、实现了imageManager:transformDownloadedImage:withURL:方法
else {
if (downloadedImage && finished) {//图像存在 且 下载完成
AppLog(@"图像存在 且 下载完成 将图像存储在磁盘");
//将图像存储在磁盘
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
图像存在 且 下载操作完成:将图片存储在磁盘
在主线程中回调completeBlock,显示图片
if (finished) {
@synchronized (self.runningOperations) {
if (strongOperation) {
//当执行完后,说明图片获取成功,可以把当前这个operation移除了。
[self.runningOperations removeObject:strongOperation];
}
}
}
下载操作完成,加锁self.runningOperations
当执行完后,说明图片获取成功,可以把当前这个operation移除了。
看下将图像存储在缓存中的方法
- (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) {
AppLog(@"将图像存储在内存中……");
NSUInteger cost = SDCacheCostForImage(image);//计算图片存储的大小
[self.memCache setObject:image forKey:key cost:cost];//将图像存储在内存中
}
1、图片不存在 || key不存在,return
2、是否使用内存缓存,默认YES,将图片存储在内存缓存
开启新线程,异步将图片缓存在磁盘中
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
//recalculate: 表示是否可以使用imageData,或者是否应该从UIImage构造新数据
//imageData为空(下载的图像若是需要transform,那么imageData就会为空)
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
//如果imageData为零(即,如果试图直接保存UIImage或在下载时转换图像)并且图像具有alpha通道,我们会认为它是PNG以避免丢失透明度
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);//图像透明度
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// But if we have an image data, we will look at the preffix
//PNG文件的前八个字节总是包含一样的签名
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
//image是png
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {//image是jpeg,压缩质量为1,无压缩
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
//其他平台,不再iPhone上,使用如下方法
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
若是图片数据处理之后,还存在,将图片缓存在磁盘
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {//检查路径是否存在
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
AppLog(@"cachePathForKey-->%@",cachePathForKey);
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
AppLog(@"fileURL-->%@",fileURL);
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];//存储图片数据
// disable iCloud backup
if (self.shouldDisableiCloud) {//iCloud 备份,默认禁用icloud备份
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
1、cachePathForKey:
/Users/xxx/Library/Developer/CoreSimulator/Devices/483F34CE-F906-4FE5-BD3A-A758DCEEB118/data/Containers/Data/Application/69141B68-DCDB-4991-A07A-0592290C5F63/Library/Caches/default/com.hackemist.SDWebImageCache.default/4ad9ae8eabfec60b40bf48f0bfc2d120.png
2、fileURL:
file:///Users/xxx/Library/Developer/CoreSimulator/Devices/483F34CE-F906-4FE5-BD3A-A758DCEEB118/data/Containers/Data/Application/69141B68-DCDB-4991-A07A-0592290C5F63/Library/Caches/default/com.hackemist.SDWebImageCache.default/4ad9ae8eabfec60b40bf48f0bfc2d120.png
(7)以上是将图片缓存在内存缓存和磁盘缓存中,以下看下图片是如何清除的
1、清除内存缓存
//清除内存缓存
- (void)clearMemory {
[self.memCache removeAllObjects];
}
2、清除磁盘缓存
//清除磁盘缓存
- (void)clearDisk {
[self clearDiskOnCompletion:nil];
}
//清除所有磁盘缓存的图像。 非阻塞方法 - 立即返回。
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
dispatch_async(self.ioQueue, ^{
//移除diskCachePath文件夹中的数据
[_fileManager removeItemAtPath:self.diskCachePath error:nil];
//重新创建缓存的路径
[_fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
3、从磁盘中删除所有过期的缓存图像。
// 从磁盘中删除所有过期的缓存图像。
- (void)cleanDisk {
[self cleanDiskWithCompletionBlock:nil];
}
// 从磁盘中删除所有过期的缓存图像。 非阻塞方法 - 立即返回。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
/*
NSURLIsDirectoryKey:判断遍历到的URL所指对象是否是目录.
NSURLContentModificationDateKey:返回遍历到项目内容的最后存取日期.
NSURLTotalFileAllocatedSizeKey:文件总的分配大小。以字节为单位的文件总分配大小(这可能包括元数据使用的空间),或者如果不可用,则为零。 如果资源被压缩,这可能小于NSURLTotalFileSizeKey返回的值。
*/
//NSDirectoryEnumerationSkipsHiddenFiles:表示不遍历隐藏文件
// This enumerator prefetches useful properties for our cache files.此枚举器为我们的缓存文件预取有用的属性。
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
//过期时间,当前时间 - 最大缓存时间(默认是一周)
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *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 *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
//resourceValuesForKeys:获取指向的文件信息
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// Skip directories.跳过跟目录
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;获取最近修改日期
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate 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 setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {//遍历过期的url
[_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. 如果我们的剩余磁盘缓存超过配置的最大大小,请执行第二次基于大小的清理通行证。 我们先删除最早的文件。
//我们当前未过期的文件大小 > 我们配置缓存文件大小 ,则删除日期最早的文件,将此清理过程的最大缓存大小的一半作为目标。
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;//将此清理过程的最大缓存大小的一半作为目标。
// Sort the remaining cache files by their last modification time (oldest first).
//按剩余缓存文件的上次修改时间排序(最早的第一个)。
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.删除文件,直到我们低于我们所需的缓存大小。
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();
});
}
});
}
4、同步删除内存和磁盘缓存中的映像
//同步删除内存和磁盘缓存中的映像
- (void)removeImageForKey:(NSString *)key {
[self removeImageForKey:key withCompletion:nil];
}
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion {
[self removeImageForKey:key fromDisk:YES withCompletion:completion];
}
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk {
[self removeImageForKey:key fromDisk:fromDisk withCompletion:nil];
}
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}
//如果允许缓存在内存,需要将内存缓存中的image移除
if (self.shouldCacheImagesInMemory) {
[self.memCache removeObjectForKey:key];
}
//如果要删除磁盘缓存中的image
if (fromDisk) {
dispatch_async(self.ioQueue, ^{
//移除磁盘缓存的路径
[_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
} else if (completion){
completion();
}
}