投稿文章,作者:missummer(简书)
上一篇我们基本上看完了SDWebImage整个工作流程,下面我们具体看一下缓存下载图片中涉及到的相关的类。
SDWebImageDownloader
SDWebImageManager实现下载依赖于下载器:SDWebImageDownloader,下载器负责管理下载任务,而执行下载任务是由SDWebImageDownloaderOperation操作完成。
SDWebImageManager实现下载 就是调用下面这个方法:
- (id)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
我们还是先来看看SDWebImageDownloader里面都写了些什么:
SDWebImageDownloader.h
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { //这个属于默认的使用模式了,前往下载,返回进度block信息,完成时调用completedBlock SDWebImageDownloaderLowPriority = 1 << 0, //渐进式下载 ,如果设置了这个选项,会在下载过程中,每次接收到一段返回数据就会调用一次完成回调,回调中的image参数为未下载完成的部分图像,可以实现将图片一点点显示出来的功能 SDWebImageDownloaderProgressiveDownload = 1 << 1, /** * 通常情况下request阻止使用NSURLCache.这个选项会默认使用NSURLCache */ SDWebImageDownloaderUseNSURLCache = 1 << 2, /** * 如果从NSURLCache中读取图片,会在调用完成block的时候,传递空的image或者imageData */ SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, /** * 系统为iOS 4+时候,如果应用进入后台,继续下载.这个选项是为了实现在后台申请额外的时间来完成请求.如果后台任务到期,操作也会被取消 */ SDWebImageDownloaderContinueInBackground = 1 << 4, /** * 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式来处理存储在NSHTTPCookieStore的cookies */ SDWebImageDownloaderHandleCookies = 1 << 5, /** * 允许不受信任的SSL证书,在测试环境中很有用,在生产环境中要谨慎使用 */ SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, /** * 将图片下载放到高优先级队列中 */ SDWebImageDownloaderHighPriority = 1 << 7, };
这些选项主要涉及到下载的优先级,缓存,后台任务执行,cookie处理以及证书认证几个方面,在创建下载操作的时候可以使用组合的选项来完成一些特殊的需求
定义里两个常量,后面通知的时候用的,这里的常量是全局常量
全局常量:不管你定义在任何文件夹,外部都能访问
const NSString *myName = @"杨千嬅染了红头发";
局部常量:用static修饰后,不能提供外界访问(只能在赋值的.m文件使用,外界不可访问)
static const NSString *myName= @"杨千嬅染了红头发"; //官方也更推荐这样定义常量 而不是用#define extern NSString *const SDWebImageDownloadStartNotification; extern NSString *const SDWebImageDownloadStopNotification;
定义了三个block
第一个返回已经接收的图片数据的大小,未接收的图片数据的大小,- (void)sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress:completed:
这个方法里面就有用到,因为图片的下载是需要时间的,所以这个block回调不止回调一次,会一直持续到图片完全下载或者下载失败才会停止回调
第二个block回调 下载完成的图片 , 图片的数据 , 如果有error返回error ,以及下载是否完成的BOOl值
第三个是header过滤:设置一个过滤器,为下载图片的HTTP request选取header.最终使用的headers是经过这个block过滤时候的返回值
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);
定义的属性
/** * 解压已经下载缓存起来的图片可以提高性能,但是会消耗大量的内存 * 默认为YES显示比较高质量的图片,如果你遇到因内存消耗过多而造成崩溃的话可以设置为NO, */ @property (assign, nonatomic) BOOL shouldDecompressImages; //下载队列最大的并发数,意思是队列中最多同时运行几条线程(全局搜索了一下,默认值是3) @property (assign, nonatomic) NSInteger maxConcurrentDownloads; /** * 当前在下载队列的操作总数,只读(这是一个瞬间值,因为只要一个操作下载完成就会移除下载队列) */ @property (readonly, nonatomic) NSUInteger currentDownloadCount; /** * 下载操作的超时时间,默认是15s */ @property (assign, nonatomic) NSTimeInterval downloadTimeout; /** * 枚举类型,代表着操作下载的顺序 */ @property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; //SDWebImageDownloaderExecutionOrder 的定义 typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { /** * 默认值,所有的下载操作以队列类型(先进先出)执行 */ SDWebImageDownloaderFIFOExecutionOrder, /** * 所有的下载操作以栈类型(后进后出)执行 */ SDWebImageDownloaderLIFOExecutionOrder }; /** * SDWeImageDownloder是一个单例,这是初始化方法 */ + (SDWebImageDownloader *)sharedDownloader; /** * 为request操作设置默认的URL凭据,具体实施为:在将操作添加到队列之前,将操作的credential属性值设置为urlCredential */ @property (strong, nonatomic) NSURLCredential *urlCredential; /** * Set username */ @property (strong, nonatomic) NSString *username; /** * Set password */**局部常量** @property (strong, nonatomic) NSString *password; /** * 设置一个过滤器,为下载图片的HTTP request选取header.意味着最终使用的headers是经过这个block过滤之后的返回值。 */ @property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
看完这些属性后我们在来看SDWebImageDownloader里面的两个核心方法,其他的方法会捎带说一下
第一个就是一开始我们说的,SDWebImageManager会调用的方法
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self; [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ //这里面都是创建下载的回调 }]; }
先来看看-addProgressCallback:completedBlock:forURL:createCallback:里面都做了些什么
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. //如果图片的url是空的就直接返回 if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } return; } 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 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(); } }); }
下面重点也是不太好理解的东西,我也是又系统地复习了一下GCD,琢磨了有段时间才继续写的
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; }
如果你GCD非常熟悉就跳过吧,不熟悉就先来看看我总结的GCD吧,写的比较好理解,先来看看 几个概念
Serial 串行 Concurrent并发
任务串行执行每次只有一个任务执行
任务并发执行就是同一时间可以有多个任务被执行
Synchronous 同步
一个同步函数只有在它完成预定的任务才返回(返回的意思是:返回当前线程,线程继续向下执行任务,你可以自己做个测试用一个同步函数,任务里面sleep(3);测试一下就明白了)
Asynchronous 异步
一个异步函数,会立即返回,预定任务会完成,但是不会等到这个任务完成才返回
Queues 队列
GCD提供 dispatch queues来处理代码,这些队列管理你提供给GCD的任务并用FIFO顺序执行,这保证了第一个被添加到队列里的任务会是队列中第一个执行的,第二个被添加的任务第二个开始执行,如此直到队列的终点
只能保证任务开始的顺序不能保证任务结束的顺序
Serial Queues 串行队列
串行队列的任务一次执行一个,每一个任务只有在前一个任务完成的时候才开始,但是你不知道一个任务(block)和下一个开始之间的时间长度
Concurrent Queues 并发队列
在并发队列中的任务能得到的保证是它们会被按照被添加的顺序开始执行,任务能以任意顺序完成,但是你不知道什么时候才开始运行下一个任务,或者任意时刻有多少block在运行,这完全取决于GCD
Queue Type 队列类型
主队列(main queue),和其它串行队列一样,这个队列中的任务一次只能执行一个,然后它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新UI的线程,这个队列就是用于发消息给UIView或发送通知的
全局调度队列(Global Dispatch Queues),它分了四种优先级(任务执行的优先级):background , low , default , high
Apple的API也会使用这些队列,所以你添加的任何任务都不会是这些队列唯一的任务
自己创建的串行队列 或者并发队列
GCD提供的函数
dispatch_async 异步 , 与其他线程无关
dispatch_sync 同步,阻塞其他线程
dispatch_apply 重复执行
dispatch_after 延迟执行
dispatch_barrier_async dispatch_barrier_sync(下面细讲)
只列举了一些常用的GCD函数,并不完全。
GCD的使用呢,总结起来就是先选用一个GCD提供的函数,传入一个你要调用的队列(三种队列类型的一种)和一个block(任务),队列会在轮到这个block执行的时候执行这个block。
注意:队列是用来存放任务的,队列并不等于线程,队列中存放的任务最后都要由线程来执行。
再回到刚才要看的部分,dispatch_barrier_sync是我们选用的GCD提供的函数,self.barrierQueue是存放任务的队列,block里面是要执行的任务
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; }
先来看看dispatch_barrier_sync
Dispatch Barrier解决多线程并发读写一个资源发生死锁
sync说明了这是个同步函数,任务不会立即返回,会等到任务执行结束才返回。
使用dispatch_barrier_sync此函数创建的任务会首先去查看队列中有没有别的任务要执行,如果有则会等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等到此方法中任务执行后才能执行,利用这个方法可以控制执行顺序。
Dispatch Barrier确保提交的block是指定队列中特定时段唯一在执行的一个.在所有先于Dispatch Barrier的任务都完成的情况下这个block才开始执行.轮到这个block时barrier会执行这个block并且确保队列在此过程 不会执行其他任务.block完成后才恢复队列。
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue",DISPATCH_QUEUE_CONCURRENT);
这是用户自己创建的队列,DISPATCH_QUEUE_CONCURRENT代表的是它是一个并行队列,为什么选择并发队列而不是串行队列我们来想一下:
串行队列可以保证任务按照添加的顺序一个个开始执行,并且上一个任务结束才开始下一个任务,这已经可以保证任务的执行顺序(或者说是任务结束的顺利)了,但是并行队列不一样,并发队列只能保证任务的开始,至于任务以什么样的顺序结束并不能保证但是并发队列使用Barrier却是可以保证的
这部分就先到这里继续向下看:
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } 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;
URLCallbacks是一个可变字典,key是NSURL类型,value为NSMutableArray类型,value(数组里面)只包含一个元素,这个元素的类型是NSMutableDictionary类型,这个字典的key为NSString类型代表着回调类型,value为block,是对应的回调
这些代码的目的都是为了给url绑定回调
继续向下看:
if (first) { createCallback(); }
如果url第一次绑定它的回调,也就是第一次使用这个url创建下载任务则执行一次创建回调
在创建回调中 创建下载操作(下载操作并不是在这里创建的),dispatch_barrier_sync执行确保同一时间只有一个线程操作URLCallbacks属性,也就是确保了下面创建过程中在给operation传递回调的时候能取到正确的self.URLCallbacks[url]值,同事确保后面有相同的url再次创建的时候if (!self.URLCallbacks[url])分支不再进入,first==NO,也就不再继续调用创建回调,这样就确保了同一个url对应的图片不会重复下载
以上这部分代码总结起来只做了一件事情:在barrierQueue队列中创建下载任务
至此下载的任务都创建好了,下面该轮到下载的操作了:
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ //创建下载的回调,我们开始来看看创建完下载的回调之后里面都写了什么事情 //配置下载超时的时间 NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } /** 创建请求对象,并根据options参数设置其属性 为了避免潜在的重复缓存(NSURLCache + SDImageCache), 如果没有明确告知需要缓存, 则禁用图片请求的缓存操作, 这样就只有SDImageCache进行了缓存 这里的options 是SDWebImageDownloaderOptions */ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; // 通过设置 NSMutableURLRequest.HTTPShouldHandleCookies = YES //的方式来处理存储在NSHTTPCookieStore的cookies request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); //返回在接到上一个请求得得响应之前,饰扣需要传输数据,YES传输,NO不传输 request.HTTPShouldUsePipelining = YES; }]; }; /** 如果你自定义了wself.headersFilter,那就用你自己设置的 wself.headersFilter来设置HTTP的header field 它的定义是 typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers); 一个返回结果为NSDictionary类型的block 如果你没有自己设置wself.headersFilter那么就用SDWebImage提供的HTTPHeaders HTTPHeaders在#import "SDWebImageDownloader.h",init方法里面初始化,下载webp图片需要的header不一样 (WebP格式,[谷歌]开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器带宽资源和数据空间) #ifdef SD_WEBP _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; #else _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy]; #endif */ if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; } /** 创建SDWebImageDownLoaderOperation操作对象(下载的操作就是在SDWebImageDownLoaderOperation类里面进行的) 传入了进度回调,完成回调,取消回调 @property (assign, nonatomic) Class operationClass; 将Class作为属性存储,初始化具体Class,使用的时候调用具体class的方法 */ operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { //progress block回调的操作 SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; /** URLCallbacks是一个字典,key是url,value是一个数组, 数组里面装的是字典,key是NSString代表着回调类型,value为block是对应的回调 确保提交的block是指定队列中特定时段唯一在执行的一个. */ dispatch_sync(sself.barrierQueue, ^{ //根据key取出装了字典的数组 callbacksForURL = [sself.URLCallbacks[url] copy]; }); for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ //根据kProgressCallbackKey这个key取出进度的操作 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; //返回已经接收的数据字节,以及未接收的数据(预计字节) if (callback) callback(receivedSize, expectedSize); }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { //completed block 回调的操作 SDWebImageDownloader *sself = wself; if (!sself) return; //依旧是根据url这个key取出一个里面装了字典的数组 __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; if (finished) { //如果这个任务已经完成,就根据url这个key从URLCallbacks字典里面删除 [sself.URLCallbacks removeObjectForKey:url]; } }); for (NSDictionary *callbacks in callbacksForURL) { //根据kCompletedCallbackKey这个key取出SDWebImageDownloaderCompletedBlock(完成的block) SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; //回调 图片 data error 是否完成的 if (callback) callback(image, data, error, finished); } } cancelled:^{ //将url对应的所有回调移除 SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; //上面 是SDWebImageDownloaderOperation *operation的创建,从这里开始就都是对operation的配置 // 设置是否需要解压 operation.shouldDecompressImages = wself.shouldDecompressImages; /** 用户认证 NSURLCredential 当连接客户端与服务端进行数据传输的时候,web服务器 收到客户端请求时可能需要先验证客户端是否是正常用户,再决定是否返回该接口的真实数据 iOS7.0之前使用的网络框架是NSURLConnection,在 2013 的 WWDC 上, 苹果推出了 NSURLConnection 的继任者:NSURLSession SDWebImage使用的是NSURLConnection,这两种网络框架的认证调用的方法也是不一样的,有兴趣的可以去google一下这里只看下NSURLConnection的认证(在这里写看着有些吃力,移步到这个代码框外面阅读) */ if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } //根据下载选项SDWebImageDownloaderHighPriority设置优先级 if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } //将下载操作加到下载队列中 [wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { /** 根据executionOrder设置操作的依赖关系 executionOrder代表着下载操作执行的顺序,它是一个枚举 SD添加下载任务是同步的,而且都是在self.barrierQueue这个并行队列中, 同步添加任务。这样也保证了根据executionOrder设置依赖关是正确的。 换句话说如果创建下载任务不是使用dispatch_barrier_sync完成的,而是使用异步方法 ,虽然依次添加创建下载操作A、B、C的任务,但实际创建顺序可能为A、C、B,这样当executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,设置的操作依赖关系就变成了A依赖C,C依赖B typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { // 默认值,所有的下载操作以队列类型执行,先被加入下载队列的操作先执行 SDWebImageDownloaderFIFOExecutionOrder, // 所有的下载操作以栈类型执行,后进先出,后被加入下载队列的操作先执行 SDWebImageDownloaderLIFOExecutionOrder }; */ [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; }
NSURLCredential 身份认证
认证过程:
1.web服务器接收到来自客户端的请求
2.web服务并不直接返回数据,而是要求客户端提供认证信息,也就是说挑战是服务端向客户端发起的
2.1要求客户端提供用户名与密码挑战 NSInternetPassword
2.2 要求客户端提供客户端证书 NSClientCertificate
2.3要求客户端信任该服务器
3.客户端回调执行,接收到需要提供认证信息,然后提供认证信息,并再次发送给web服务
4.web服务验证认证信息
4.1认证成功,将最终的数据结果发送给客户端
4.2认证失败,错误此次请求,返回错误码401
Web服务需要验证客户端网络请求
NSURLConnectionDelegate 提供的接收挑战,SDWeImage使用的就是这个方案
-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
至此下载管理 SDWebImageDownloader到这里就算结束了,它的主要作用就是创建下载任务,管理下载任务(取消,下载等状态改变)这里的重点就是对self.barrierQueue的理解,最后我们来看看SDWebImageDownloaderOptions下载操作和下载过程的实现
SDWebImageDownloaderOptions
它的作用就是网络请求的配置,进行网络请求以及数据处理
依旧先来看看它公开声明的属性和方法
@interface SDWebImageDownloaderOperation : NSOperation /** * 下载时用于网络请求的request */ @property (strong, nonatomic, readonly) NSURLRequest *request; /** * 图片下载完成是否需要解压 */ @property (assign, nonatomic) BOOL shouldDecompressImages; /** * :URLConnection是否需要咨询凭据仓库来对连接进行授权,默认YES */ @property (nonatomic, assign) BOOL shouldUseCredentialStorage; /** * web服务要求客户端进行挑战,用NSURLConnectionDelegate提供的方法接收挑战,最终会生成一个挑战凭证,也是NSURLCredential的实例 credential */ @property (nonatomic, strong) NSURLCredential *credential; /** * SDWebImageDownloader.h里面定义的,一些下载相关的选项 */ @property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options; /** * 预期的文件大小 */ @property (assign, nonatomic) NSInteger expectedSize; /** * connection对象进行网络访问,接收到的response */ @property (strong, nonatomic) NSURLResponse *response; /** * 用默认的属性值初始化一个SDWebImageDownloaderOperation对象 */ - (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock;
然后继续看SDWebImageDownloaderOperation.h
初始化方法,这个就是初始化一个SDWebImageDownloaderOperation实例,没什么看点
- (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock { if ((self = [super init])) { _request = request; _shouldDecompressImages = YES; _shouldUseCredentialStorage = YES; _options = options; _progressBlock = [progressBlock copy]; _completedBlock = [completedBlock copy]; _cancelBlock = [cancelBlock copy]; _executing = NO; _finished = NO; _expectedSize = 0; responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called } return self; }
但是下面这个方法- (void)start就是关键了,它是对NSOperation- (void)start的重写,这个方法是执行下载任务的核心代码
- (void)start { //先加一把线程锁,保证执行到这里的时候只有当前线程在执行下面的方法 @synchronized (self) { //如果下载操作被取消了 if (self.isCancelled) { self.finished = YES; //把下载相关的属性置为nil [self reset]; return; } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 /** App 进入后台时,请求继续执行一段时间的方法, 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系统借用一点时间, 继续执行下面的代码来完成connection的创建和进行下载任务。 */ 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 // 下载任务执行的状态,在执行是YES,不在执行时NO self.executing = YES; //创建用于下载的connection self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; //获取当前得得线程 self.thread = [NSThread currentThread]; } //开始下载 [self.connection start]; //如果connection创建完成 if (self.connection) { if (self.progressBlock) { //任务开始立刻执行一次进度的回调 self.progressBlock(0, NSURLResponseUnknownLength); } dispatch_async(dispatch_get_main_queue(), ^{ //发送开始下载的通知 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); /** 在 [self.connection start];有返回结果(正常完成,有错误都算是结果)之前, 代码会一直阻塞在CFRunLoopRun()或者CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false) 这里, 也就是说 [self.connection start];之后下载就一直在进行中,一直到下载完成或者出错了(这两种情况都会调用CFRunLoopStop),这个阻塞才会解除 */ if (floor(NSFoundationVersionNumber) = __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 }
最后,我们来看NSURLConnection (delegate)
1)connection: didReceiveResponse:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { //如果statusCode<400并且不等304 if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { //设置文件的预期大小,如果response.expectedContentLength >0那么预期文件的大小就是response.expectedContentLength ,反之就是0 NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; //立即完成一次进度回调 if (self.progressBlock) { self.progressBlock(0, expected); } //初始化属性imageDate,用于拼接图片 二进制数据 self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; self.response = response; dispatch_async(dispatch_get_main_queue(), ^{ //异步的 向主队队列发送一个通知 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; }); } else { NSUInteger code = [((NSHTTPURLResponse *)response) statusCode]; /** 如果 statusCode == 304 就调用[self cancelInternal]方法 ,或者取消self.connection的连接 取消操作,发送操作停止的通知,执行完成回调,停止当前的runloop,设置下载完成标记为YES,正在执行的为NO,将属性置为空 */ if (code == 304) { [self cancelInternal]; } else { [self.connection cancel]; } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); } CFRunLoopStop(CFRunLoopGetCurrent()); [self done]; } }
2)connection: didReceiveData拼接数据的协议
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.imageData appendData:data]; if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { // 根据self.imageData获取已接收的数据的长度 const NSInteger totalSize = self.imageData.length; /** 每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理, 而且这个数据应该是已接收的全部数据,而不仅仅是新的字节,所以才使用self.imageData作为参数(注意创建imageSource使用的数据是CoreFoundation的data,但是self.imageData是NSData,所以用(__bridge CFDataRef)self.imageData做转化 ) */ CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); /** 在首次接收到数据的时候,图片的长宽都是0(width+height == 0) 先从这些包含图像信息的数据中取出图像的长,宽,方向等信息以备使用 */ if (width + height == 0) { //获取图片的属性信息 CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { NSInteger orientationValue = -1; //图片像素的高度 可以前面加(__bridge NSNumber *)转换为NSNumber类型 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); //CoreFoundation对象类型不在ARC范围内,需要手动释放资源 CFRelease(properties); /** 使用Core Craphics框架绘制image时,使用的是 initWithCGImage这个函数,但是使用这个函数有时候会造成图片朝向的错误, 所以在这里保存朝向信息,orientation是一个可以记录图片方向的枚举 */ orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } } /** width+height>0 说明这时候已经接收到图片的数据了 totalSize < self.expectedSize 说明图片 还没有接收完全 */ if (width + height > 0 && totalSize < self.expectedSize) { // 创建图片 CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE // Workaround for iOS anamorphic(失真的 , 变形的) image 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(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } CFRelease(imageSource); } if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); } }
3.connectionDidFinishLoading:这个方法完成以后,代理不再会接收人和connection发送的消息,标志着图片下载完成,一般下载任务正常结束之后就会执行一次这个方法
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; @synchronized(self) { /** 停止当前的runLoop,将connection属性和thread属性 发送下载停止的通知 */ 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]; }); } /** 检查sharedURLCache是否缓存了这次下载response 如果没有就把responseFromCached设置为NO */ if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) { responseFromCached = NO; } /** 执行完成回调 */ if (completionBlock) { /** 图片的缓存用的都是SDWebCache,所以就算设置了SDWebImageDownloaderIgnoreCachedResponse, responseFromCached 回调的图片也是nil(理解有可能有偏差) */ if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { completionBlock(nil, nil, nil, YES); } else if (self.imageData) { //将数据转换为UIImage类型 UIImage *image = [UIImage sd_imageWithData:self.imageData]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; image = [self scaledImageForKey:key image:image]; // 注意对于gif图片,不需要解压缩 if (!image.images) { if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { //如果图片的大小为0 , 完成回调报错 completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); } else { //回调图片 已经图片的大小 完成状态YES 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; //将NSConnection 设置为完成状态 [self done]; }
到这里,看的也差不多了,认真看完感觉这个作者太厉害了,也真的学习到了很多,欢迎交流,也希望大家自己有空了也看一下,这次真的是拖了一个月因为有的东西我没明白就看了好多天也查了 各种资料,这次也算是尽力写好了吧,惭愧
包厢里的狂欢,曲终人散
have Fine
以上