SDWebImage源码浅析
1、平时开发的过程中用到过三方库吗?
2、使用三方库的过程中遇到过什么问题吗?
3、有读过优秀三方的源码么?
4、知道三方库底层怎么实现的吗?
写在开始之前
在很多水友相亲的过程中,经常会被问到类似的问题,有些人能够言简意赅的把某框架的优缺点表达出来(心中:我凑,还好我昨天背了一下,这个逼我一定要装好)
,有些人却还是停留在简单使用API的阶段,具体怎么实现却支支吾吾的说不清楚(心中万马奔腾,麻痹的,这么底层的东西也要问吗?)
。
iOS日常开发中,常用的开源三方库有很多AFNetworking、SDWebImage、MJRefresh、YYKit系列等,今天我们就先来说说SDWebImage。
SDWebImage的源码第一次看还是大概2年前,当时还是用的NSURLConnection
来下载图片的,时光荏苒,SDWebImage早已改变成了NSURLSession
来下载图片,并且不断的优化,Github也多了1W多Star,而我还是当年那个小菜逼,说多了都是眼泪
最近一次面试的时候,被问到一个问题,
面试官:UITableView的5个cell同时下载一个相同图片,SDWebImage底层怎么处理的?
我:之前看过,记不清了(当时我的表情是懵逼的,之前只是草草的看过一遍,而且还有很多地方都看不懂)
面试官:没关系,如果要是你自己做,你会怎么做?
我:弄一个串行队列,第一个任务下载完之后,缓存,然后后边的任务就可以直接取缓存
面试官:如果我同时需要5个图片的下载进度呢?
我:当是真心有点凌乱了,然后思(懵)考(逼)了2分钟
面试官:好吧,今天的面试就先到这里吧
带着面试的问题,我老老实实的又从GitHub上下载个SDWebImage-4.2.2,花了一天的时间,又看了一遍,现在把看明白的东西记录一下。
1、SDWebImage工作流程
这是GitHub上提供的,我顺手给牵了过来,再说了,文化人的事,能叫偷么?虽然SDWebImage的主要工作流程很多水友都能说出个大概,我还是简单说一下我的理解吧,像我这种大龄程序员,说不定哪天就忘了,将来还能回来翻翻笔记。
Step1:调用加载图片API
sd_setImageWithURL:
系列
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
调用API之后,SDWebImage会判断是否显示占位图,如果让显示就先显示占位图
// 如果options != SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
写到这里再啰嗦几句,说出来你可能不信,options & SDWebImageDelayPlaceholder
这种写法,在昨天之前我是看不懂的,看到逻辑与&,我努力回想了下当年的秃顶计算机老师是怎么讲的,但是没想起来,后来琢磨了下,应该是0 & 0 = 0, 1 & 0 = 0, 1 & 1 = 1
,结合SDWebImageDelayPlaceholder
的定义
* By default, placeholder images are loaded while the image is loading. This flag will delay the loading
* of the placeholder image until after the image has finished loading.
*/
SDWebImageDelayPlaceholder = 1 << 9,
1 << 9 ==> 0000 0000 0000 0001 << 9 ==> 0000 0010 0000 0000
假设options = SDWebImageRetryFailed = 1 << 0,
options & SDWebImageDelayPlaceholder
==> 0000 0000 0000 0001
& 0000 0010 0000 0000
===========================
0000 0000 0000 0000 = 0
假设options = SDWebImageDelayPlaceholder = 1 << 9
options & SDWebImageDelayPlaceholder
==> 0000 0010 0000 0000
& 0000 0010 0000 0000
===========================
0000 0010 0000 0000 = 1 << 9 = !0
option逻辑与(&)上自己本身结果是自己,非零,options逻辑与(&)上非自身的其他枚举值,结果都是0,这个逻辑于(&)在这里跟 == 的作用是一样的,就是判断options的枚举值,
options & SDWebImageDelayPlaceholder ==> options == SDWebImageDelayPlaceholder
what the fk?
原来只是个options == SDWebImageDelayPlaceholder判断,哎,都是吃文化低的亏,啰嗦了这么多也不知道表达清楚没。
Step2:SDImageCache以URL为key在imageCache中查找图片
首先在memCache中查找,如果能找到image,执行回调block,把image传回去,如果memCache中没有找到,会开启一个异步线程去磁盘上查找,如果找到image,保存到memCache中,如果没有找到,返回nil,查找完成。
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if (image.images) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
Step3:SDWebImageDownloader下载图片
如果内存和磁盘上都没有查询到URLString对应的image,就会让imageDownloader去下载图片,根据URL创建一个request,然后根据request创建一个sessionTask开始下载
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[sself.downloadQueue addOperation:operation];
operation添加到operationQueue中,自动开启下载任务,下载的过程中通过进度block回传下载进度,下载完成后解码转码,调用下载完成回调block把image对象回传。
Step4:SDImageCache存储图片
默认转码后的图片会缓存到内存中,如果同时需要缓存到磁盘上,才会开启异步IO队列通过NSFileManager把图片写入到本地磁盘,磁盘上图片的名字是经过MD5处理后的URLString。
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, use PNG format
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
}
[self storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
[self checkIfQueueIsIOQueue];
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
这就是SDWebImage加载一张图片的大致流程了,其实SDWebImage里面做了很多细节优化处理。让我们接着往下look
3、SDWebImage底层优化
- 1、无效URL的处理
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
SDWebImageManager维护了一个黑名单存放图片下载失败的URL,每次根据URLString查询图片的时候,会先去黑名单中查询,目标URL是否在黑名单中
BOOL isFailedUrl = NO;
if (url) {
//为了线程安全
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
如果URL在黑名单中,直接执行回调Block,回传error,提高效率,避免不必要的操作。如果不被黑名单包含,继续正常流程,对应的URL下载失败后,把URL添加到黑名单
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
- 2、高并发相同URL请求的处理
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
SD内部每一个下载请求,对应一个SDWebImageDownloaderOperation,SDWebImageDownloader通过URLOperations属性来维护这个operation。
dispatch_barrier_sync(self.barrierQueue, ^{
//根据URLString查询是否有正在下载的操作
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
operation = createCallback();
//把下载操作添加到URLOperations中
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
//下完成后,把该URL的下载操作从URLOperations移除
[self.URLOperations removeObjectForKey:url];
};
});
};
}
//已经有该URLString对应的下载任务存在,保存新任务的进度回调block和完成回调block
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
在NSURLSessionDataDelegate
的- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
方法中可以看到如下代码
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
//循环执行相同URL的进度回调Block
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
简单的说,就是相同的URL只开启一个下载任务,在下载的过程中,把下载进度分别通知给该URL对应的其他操作,既节约流量、又兼顾所有任务的下载进度。下载完成的回调block同理执行。
- 3、高并发不同URL请求的处理
UITableView的多个cell同时加载图片的时候,就会出现高并发的情况
//默认最大并发数
_downloadQueue.maxConcurrentOperationCount = 6;
//session的初始化
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
系统API对delegateQueue参数的描述是If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
SDWebImage的高并发下载任务是在一个并行队列,默认支持最大的并发数是6,默认并发任务执行顺序是FIFO(first in first out),如果设置任务的执行顺序为LIFO(last in first out)
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
//设置操作之间的依赖,新添加的operation被旧的operation依赖,来实现后进先出
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
SDWebImageDownloader
在接收到NSURLSessionDataDelegate
代理方法回调的时候,通过NSURLSessionDataTask
获取到对应的SDWebImageDownloaderOperation
,把delegate方法转发给SDWebImageDownloaderOperation
,避免数据错乱。
...
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
//把代理方法转发给SDWebImageDownloaderOperation
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
//根据NSURLSessionTask获取对应的SDWebImageDownloaderOperation
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
SDWebImageDownloaderOperation *returnOperation = nil;
for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
return returnOperation;
}
...
- 4、缓存管理策略
SDWebImage的内存管理由SDImageCache负责,分别监听了内存警告、应用将释放、进入后台三个通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
//收到内存警告通知,把内存中缓存的图片清空
- (void)clearMemory {
[self.memCache removeAllObjects];
}
SDImageCache图片磁盘缓存的时长默认是1周
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
在每次收到进入后台、应用将要释放通知后,SDWebImage会检查磁盘上的图片,如果过期就清理
// Remove files that are older than the expiration date;
//如果图片过期,记录过期图片URL
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
//循环删除过期图片
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
如果设置最大的缓存空间,在收到进入后台、应用将要释放通知后,判断使用当前空间使用超过设置的最大空间的50%后,开始清理,按照修改时间排序后,从修改时间最早的开始清理,直到使用空间小于缓存空间50%后结束。
// 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.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
NSArray<NSURL *> *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<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
-
5、解码转码
说实话,解码转码这块看的还不太明白,等我看明白了,再回来补上...