SDWebImage源码详解 - 异步下载器SDWebImageDownloader
SDWebImage的图片下载是由SDWebImageDownloader这个类来实现的,它是一个异步下载管理器,下载过程中增加了对图片加载做了优化的处理。而真正实现图片下载的是自定义的一个Operation操作,将该操作加入到下载管理器的操作队列downloadQueue中,Operation操作依赖系统提供的NSURLConnection类实现图片的下载。
我们来看一看这个异步下载器SDWebImageDownloader的具体实现:
//在这里由枚举定义了不同的下载选项在下载的过程中,程序会根据不同的下载选项,而执行不同的操作
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,
//默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
//如果从NSURLCache缓存中读取图片,则使用nil作为参数来调用完成block
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 在iOS 4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允许不受信任的SSL证书。主要用于测试目的
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 将图片下载放到高优先级队列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
//下载管理器还提供了两种下载顺序,以控制下载操作的顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序
SDWebImageDownloaderFIFOExecutionOrder,
// 以栈的方式,按照后进先出的顺序下载。
SDWebImageDownloaderLIFOExecutionOrder
};
//外部定义的系统通知标示
extern NSString *const SDWebImageDownloadStartNotification;
extern NSString *const SDWebImageDownloadStopNotification;
//每个下载操作都定义了回调操作,如下载进度回调,下载完成回调,头部过滤等,这些回调操作是以block形式来呈现
//每个下载操作的下载进度回调和下载完成回调,这两个回调稍后将保存在下载管理器的URLCallbacks字典中,key为URL,value为一个数组,数组里面又存放一个保存了下载进度回调和完成回调代码块的字典。
//这个字典数组同时也保证了同一张图片只会被下载一次
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
@interface SDWebImageDownloader : NSObject
//是否解压图片,默认为YES,此项操作可以提升性能,但是需要消耗较多内存
@property (assign, nonatomic) BOOL shouldDecompressImages;
//队列最大并发数
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
//当前正在或者待执行的下载任务数
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
//每个下载操作的超时时间
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
//下载操作的执行顺序,默认是SDWebImageDownloaderFIFOExecutionOrder先进先出顺序
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
//下载管理器单例函数
+ (SDWebImageDownloader *)sharedDownloader;
//设置下载操作的请求凭证
@property (strong, nonatomic) NSURLCredential *urlCredential;
//设置用户名
@property (strong, nonatomic) NSString *username;
//设置密码
@property (strong, nonatomic) NSString *password;
//http头部的过滤函数
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
//设置http请求头部字段
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
//返回http请求头部字段
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
//设置图片下载的操作。默认使用SDWebImageDownloaderOperation这个自定义的下载操作
- (void)setOperationClass:(Class)operationClass;
//创建一个异步下载器,
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
//暂停下载队列
- (void)setSuspended:(BOOL)suspended;
下载管理器的主要实现为downloadImageWithURL:options:progress:completed:
方法,这个方法调用- (void)addProgressCallback:completedBlock:forURL: createCallback:
方法来将请求的信息和一些回调函数存入管理器中,同时在创建回调的block中创建新的operation操作,新的操作由管理器中存储的信息配置后,放入到downloadQueue操作队列中,最后返回新创建的操作。接下来看一下这个函数的具体实现:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
//调用`downloadImageWithURL:options:progress:completed:`方法创建operation操作
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
//下载操作的超时时间.默认是15.0S
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
//创建请求对象,并根据options参数设置其属性
//为了避免潜在的重复缓存(NSURLCache + SDImageCache),如果没有明确告知需要缓存,则禁用图片请求的缓存操作
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;
}
//创建SDWebImageDownloaderOperation操作对象,并进行配置
//配置信息包括是否需要认证、优先级
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
//从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
//从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用,
// 如果finished为YES,则将该url对应的回调信息从URLCallbacks中删除
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
//取消操作将该url对应的回调信息从URLCallbacks中删除
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
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;
}
//加入到NSOperationQueue队列中,自动调用start函数
//将操作加入到操作队列downloadQueue中
// 如果是LIFO顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
[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;
}
//这个方法主要是保存传入下载管理器的下载进度回调或完成回调,然后创建新的回调操作
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
//url不能为空
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
//以dispatch_barrier_sync操作来保证同一时间只有一个线程能对URLCallbacks进行操作
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same URL
//处理同一URL的同步下载请求的单个下载
//每个下载操作的下载进度回调和下载完成回调,这两个回调稍后将保存在下载管理器的URLCallbacks字典中,
//key为URL,value为一个数组,数组里面又存放一个保存了下载进度回调和完成回调代码块的字典。
//这个字典数组同时也保证了同一张图片只会被下载一次
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
//执行创建好的新的回调代码
if (first) {
createCallback();
}
});
}
下载操作
每个图片的下载都是一个Operation操作,由下载管理器来管理每个Operation的具体执行,接下来看一下图片下载操作的具体实现,
首先SDWebImage定义了一个协议,即SDWebImageOperation,作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
然后SDWebImage自定义实现了一个Operation类:SDWebImageDownloaderOperation,遵循SDWebImageOperation协议,这个类对外公开了一个初始化方法:initWithRequest:options:progress:completed:cancelled:
.图片的真实下载操作依赖系统提供的NSURLConnection类。可以看到SDWebImageDownloaderOperation实现了NSURLConnectionDataDelegate协议。这里不过多介绍NSURLConnection的使用,具体使用可以参看这里,稍后我们将主要介绍两个代理方法,-connection:didReceiveData:
和-connectionDidFinishLoading:
.
#######start 方法
既然在下载管理器中使用NSOperationQueue来管理下载操作对象SDWebImageDownloaderOperation,这样我们就需要实现自定义Operation的main方法或者start方法,(关于start方法和main方法,有这么一句话【如果你要处理的非并发的操作,你需要实现-main方法,如果你要处理的是并发的操作,你需要实现-start方法】),所以SDWebImageDownloaderOperation实现了start方法,(注意:重写“start”方法需要相对复杂的实现,需要注意像isExecuting,isFinished,isConcurrent和isReady这些属性)。start方法创建了我们下载所使用的NSURLConnection对象,开启了图片的下载,同时抛出一个下载开始的通知。当然,如果我们期望下载在后台处理,则只需要配置我们的下载选项,使其包含SDWebImageDownloaderContinueInBackground选项。start方法的具体实现如下:
- (void)start {
//管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//开启后台下载
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
//如果在规定时间内未完成,则执行此代码块
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//在主线程中发送,开始下载通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
//开启Runloop
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
//线程开始的时候,connect还未结束,则取消连接
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else { //连接未成功,执行错误回调
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
代理方法
这里SDWebImageDownloaderOperation自定义操作实现了NSURLConnectionDataDelegate协议的以下几个方法:
//接收到服务器响应的时候调用这个方法,这里主要对304返回代码,做一下处理
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
//接收到服务器返回数据时调用,多次调用。这里主要对接受到的图片数据做一下处理,稍后详细介绍
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
//图片数据接受完毕时调用,调用完成回调函数处理图片
- (void)connectionDidFinishLoading:(NSURLConnection *)Connection;
//连接失败是调用
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
//设置缓存数据
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
//是否使用请求认证
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection
//服务端要求客户端接收认证
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
我这里主要介绍这两个两个代理方法,-connection:didReceiveData:
和-connectionDidFinishLoading:
.
//当每次接收服务传回的数据时就会调用此方法。
//每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理。
//在首次获取到数据时,也就是图片的长宽都为0(width+height==0),先从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。
//而后在图片全部下载完成之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。
//如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
//已接收的数据长度
const NSInteger totalSize = self.imageData.length;
//更新数据源,我们必须通过所有的数据,而不仅仅是新的字节
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
if (width + height == 0) { //刚开始时,图片宽高都为0
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
//从这些数据中获取图片的长、宽、方向属性值
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
//当绘制到Core Graphics时,我们会丢失方向信息,
//这意味着有时候由initWithCGIImage创建的图片的方向会不对,
//所以在这边我们先保存这个信息并在后面使用。
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
//图片还未下载完成,在下载中。。。
if (width + height > 0 && totalSize < self.expectedSize) {
//使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// Workaround for iOS anamorphic image
//适用于iOS变形图像的解决方案。
//由于iOS只支持RGB颜色空间,所以在此对下载下来的图片做个颜色空间转换处理
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
//对图片进行缩放、解码操作
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{ //这里怎么会执行completedBlock?
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
再来看一下-connectionDidFinishLoading:
方法,当数据下载完成后,将调用此方法,方法调用的时间点,决定了这个方法主要对图片做下载完成的处理,以及操作环境的清理。
- (void)connectionDidFinishLoading:(NSURLConnection *)Connection {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
//停止unloop
@synchronized(self) {
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
//在主线程中发送下载完成和停止的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
});
}
if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}
//如果有完成回调,则处理完成回调
if (completionBlock) {
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
completionBlock(nil, nil, nil, YES);
} else if (self.imageData) {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
//对于gif图片不做解码操作
if (!image.images) {
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {
completionBlock(image, self.imageData, nil, YES);
}
} else {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
}
}
self.completionBlock = nil;
//清理操作环境
[self done];
}
图片下载的实现是主要是实现一个自定义的Operation操作对象,每一个图片下载对应一个<strong>操作对象</strong>,然后通过<strong>下载管理器</strong>来统一管理这些操作对象。我的理解:下载管理器的主要作用是,统一管理每个图片下载的Operation配置,针对每个自定义Operation操作,管理和设置不同的下载进度回调和完成回调,使得自定义的Operation操作只需关注单个的操作即可,降低了代码耦合。。