怎样进行图片下载?
图片下载真正的动作是在这里
//SDWebImageDownloader.m
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
//addProgressCallback...方法主要是将progressBlock(过程回调)和completedBlock(结束回调)保存起来。
//以url为key保存到SDWebImageDownloader的URLCallbacks里面,供后面使用。
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.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
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
operation = [[wself.operationClass alloc] initWithRequest:request
inSession:self.session
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
//过程回调
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
//结束回调
}
cancelled:^{
//取消回调
}
];
operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
从代码可以看出,先是创建了一个SDWebImageDownloaderOperation
的实例operation
,然后把它添加到下载队列downloadQueue
中。SDWebImageDownloaderOperation
继承于NSOperation
,重写了start
方法,downloadQueue
在添加后会尽快开始执行,去调start
方法。(NSOperation介绍)
紧接着SD下载图片是通过NSURLSession和NSURLSessionTask配合来完成的。NSURLSession
是在SDWebImageDownloader
里面实例化的,然后传入给SDWebImageDownloaderOperation
,作为它的一个属性,叫session
。然后session
用来实例化dataTask
。(为了防止到这里session
还没初始化,所以在里面又做了一层判断,如果还没初始化,那么我就初始化一个,并且跟外面传进来的区分开,用完后也由我自己释放。)之后启动任务开始下载图片。(NSURLSession介绍)
//代码节选自SDWebImageDownloaderOperation.m
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic) NSURLSession *ownedSession;
- (void)start {
//.......
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
//.......
[self.dataTask resume];
//.......
}
- (void)reset {
self.cancelBlock = nil;
self.completedBlock = nil;
self.progressBlock = nil;
self.dataTask = nil;
self.imageData = nil;
self.thread = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
那如何取消图片下载呢?
从上文看,取消下载图片的线程只要调用SDWebImageDownloader
的- (void)cancelAllDownloads
方法,这个方法会调用downloadQueue
的cancelAllOperations
方法取消。
SDWebImageManager的runningOperations干嘛用的?
runningOperations
是一个数组,里面的元素是SDWebImageCombinedOperation
,这个类直接继承NSObject,实现了SDWebImageOperation
(只是为了拥有一个cancel方法)。
@interface SDWebImageCombinedOperation : NSObject
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;
@end
runningOperations
用于标示应用目前有多少个正在获取图片的操作(记住,不是NSOperation
,也不是下载图片这个动作本身)。当用户所有正在获取图片的操作都不想要了的情况,可以调用- (void)cancelAll
方法,这个方法会对runningOperations
里面的子元素都执行cancel
方法,之后清空这个数组。
//SDWebImageCombinedOperation
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}
什么?这里也有个取消?那跟上面讲的取消图片下载有什么关系?
我们看下面代码
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
//...
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
//....
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//...
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
//...
id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {//....}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
//....
return operation;
}
operation.cacheOperation
其实是获取缓存的一个NSOperation
,确切的说应该是从磁盘获取图片,但是在这里并没有像图片下载使用到的一样,这里只是作为一个是否取消的标示而已。当调用- (void)cancelAll
方法时,operation.cacheOperation
的取消是为了取消缓存图片的获取。
但是注意到这里还有一个operation.cancelBlock
。调用- (void)cancelAll
方法是会执行cancelBlock
的。那这个是干嘛的。
上面已经讲到,下载图片的operation
是由SDWebImageDownloader
的- (id
方法返回的,返回的operation
恰好是给operation.cancelBlock
里面调用。cancelBlock
执行时,就直接把下载图片的operation
给取消了。
所以SDWebImageManager
调用- (void)cancelAll
后,1会取消从磁盘加载缓存图片,2会取消图片下载动作。
缓存如何搜索到?
缓存搜索是在SDImageCache
的- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
方法中进行的,我们来看下代码
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
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 {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
从内存获取缓存是指从系统提供的NSCache
的一个对象memCache
获取。内存缓存这里使用的不是集合,我想应该是NSCache
可以设置totalCostLimit
和countLimit
,这两个属性可以在内存管理更加自动化些。(NSCache介绍)
从磁盘获取缓存的时候是先用NSOperation
实例化了一个operation
对象,operation
传给外面,用于控制磁盘获取是否取消。磁盘取到对象后会根据缓存策略决定是否将图片保存到内存中。
如何从磁盘找到缓存的图片?
看下面这个方法
- (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;
}
}
上面方法获取到data
之后,转为image
,并根据是否解压设置image
。重点的还是下面这个方法
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
if (data) {
return data;
}
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
if (imageData) {
return imageData;
}
}
return nil;
}
从方法名就可以知道是查找所有的路径,找到data后返回。这些路径包括defaultPath
和customPaths
。defaultPath
可以自定义设置,如果没有系统会默认创建。值得一提的是为了确保data
的唯一性,SD使用CC_MD5
的方式对key
做了加密,然后用来作为文件的名字。
为毛可以控制到图片的下载进度呢?
上文提到,SD下载图片是通过NSURLSession
和NSURLSessionTask
配合来完成的。进度的控制归功于以下的方法
//response带有目标文件的大小,可以从这个里面获取到。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
//只要接收到数据,就会调用这个方法,所以这个方法是重复调用的。
//可以从这里获取到单次接收了多少,然后保存到内存变量imageData,这样就知道已经接收到了多少。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
有了总量和单次接收量自然可以知道进度,这时候再调用回调处理就可以了。
SD的内存和性能处理?
内存:进入后台时/程序退出(UIApplicationWillTerminateNotification),会对过期图片(什么是过期图片?就是已经缓存超过maxCacheAge
时间的那些图片)进行删除。删除之后如果剩下图片占有的大小大于最大大小(maxCacheSize
)的一半,那么会根据图片修改时间排序后,删除旧图片,直到大小满足。当程序收到内存报警时,将内存都删掉。详细代码见- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock
方法。
性能:图片的下载都是开新的异步线程。
//来源SDImageCache.m
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
//下面两个方法都会调用
//- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock方法
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
听说下载失败后可以重新下载?
SD是有一个SDWebImageOptions
叫SDWebImageRetryFailed
,但是这不意味着失败后会自己去重新尝试下载。
SDWebImageManager
有一个数组failedURLs
,用于存放所有下载图片失败的url,当加载图片的时候遇到上次失败过的url并且options没有设置为SDWebImageRetryFailed
是直接不做处理的,反之才会根据失败的url重新加载。
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
//....
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
//....
}
听说SD里面图片的唯一标示可以自定义?
SDWebImageManager
提供了一个cacheKeyFilter
供程序员设置,之后在要对图片操作前,会先根据url调用以下方法获取到唯一标示再进行后续操作。
- (NSString *)cacheKeyForURL:(NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return [url absoluteString];
}
}
官方给出了设置的方式
[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
}];
YYImage是如何做到逐行扫描的?
一般来说,图片下载,是通过将图片数据转换为NSData类型,然后顺序进行传递,接收到后再拼接的。YYImage的逐行扫描是因为图片本身有一个属性interlace,只有设置了这个属性才可能实现。(参考http://blog.csdn.net/code_for_free/article/details/51290067 )
如果本文让你有那么点觉得“I get”的感觉,请点个赞呗!写作很辛苦,路过请点赞!