iOS----------SDWebimage源码解析(5)
2016-03-10 23:11
423 查看
本篇我们来介绍下载图片的类,创建SDWebImageDownloaderOperation对象,它继承于NSOperation,遵守SDWebImageOperation协议,下面我们来看看SDWebImageDownloaderOperation类的源码。使用SDWebImageDownloaderOperation来封装任务。
1、SDWebImageDownloaderOperation.h文件
一些属性大家从源码中就可以看到,有一个主要的方法
2、SDWebImageDownloaderOperation.m文件
看了SDWebImageDownloaderOperation.m文件才发现真正的核心代码并不是初始化方法,在init方法中只是对传入的参数的赋值和获取
(2)、重写operation的start方法
上面方法中创建了一个NSURLConnection对象,苹果已经将NSURLConnection用NSURLSession代替,但在SDWebimage中还没有替换,可能下一个版本会替换吧。创建NSURLConnection对象后开始下载图片。
开始下载图片后,会调用NSURLConnection代理中的方法
第二个代理方法,获取到Data
下载完成的代理
发生错误的代理
// 如果我们需要对缓存做更精确的控制,我们可以实现一些代理方法来允许应用来确定请求是否应该缓存
// 如果不实现此方法,NSURLConnection 就简单地使用本来要传入 -connection:willCacheResponse: 的那个缓存对象,
所以除非你需要改变一些值或者阻止缓存,否则这个代理方法不必实现
认证相关的东东
// 当客户端向目标服务器发送请求时。服务器会使用401进行响应。客户端收到响应后便开始认证挑战(Authentication Challenge),而且是通过willSendRequestForAuthenticationChallenge:函数进行的。
// willSendRequestForAuthenticationChallenge:函数中的challenge对象包含了protectionSpace(NSURLProtectionSpace)实例属性,在此进行protectionSpace的检查。当检查不通过时既取消认证,这里需要注意下的是取消是必要的,因为willSendRequestForAuthenticationChallenge:可能会被调用多次。
以上为下载image 的代码,重要代码解析都在上面,这个大家应该比较熟悉,经过前几篇的介绍,这些代码的阅读应该不会很困难。
下一篇我们将做扫尾工作,将SDWebimage中的其它常用的类和方法在过一遍,这样我们对SDWebimage的理解会更加深刻。
1、SDWebImageDownloaderOperation.h文件
一些属性大家从源码中就可以看到,有一个主要的方法
- (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock;
2、SDWebImageDownloaderOperation.m文件
看了SDWebImageDownloaderOperation.m文件才发现真正的核心代码并不是初始化方法,在init方法中只是对传入的参数的赋值和获取
- (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; }
(2)、重写operation的start方法
- (void)start { //线程同步加锁 @synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 //获取系统的application Class UIApplicationClass = NSClassFromString(@"UIApplication"); BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { __weak __typeof__ (self) wself = self; //获取单例app UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; //获取这个后台线程的标示UIBackgroundTaskIdentifier self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ __strong __typeof (wself) sself = wself; //后台下载时间到了,就会调用这个block,如果任务仍在下载就进行取消,调用endBackgroundTask这个方法通知系统该backgroundTaskId停止,并把backgroundTaskId的状态改为无效 if (sself) { [sself cancel]; [app endBackgroundTask:sself.backgroundTaskId]; sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; } #endif self.executing = YES; //创造connection 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]; }); 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(); } 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 }
上面方法中创建了一个NSURLConnection对象,苹果已经将NSURLConnection用NSURLSession代替,但在SDWebimage中还没有替换,可能下一个版本会替换吧。创建NSURLConnection对象后开始下载图片。
开始下载图片后,会调用NSURLConnection代理中的方法
#pragma mark NSURLConnection (delegate) - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { //'304 Not Modified' is an exceptional one // 这里有对response的处理 if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { //获取返回数据的长度 NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; if (self.progressBlock) { self.progressBlock(0, expected); } //创建一个收集数据的空间 NSMutableData 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]; //This is the case when server returns '304 Not Modified'. It means that remote image is not changed. //In case of 304 we need just cancel the operation and return cached image from the cache. //如果code是304表示新加载的数据与原来的相同,那么就不需要发送请求,可以从缓存中取到 //场景:如果从后台请求的图片地址没有变化,图片发生了变化,在请求这张图片的时候需要设置这张图片在加载的时候刷新缓存,如果图片发生变化那么就会去调用请求,如果没有变化那么那么这里会拦截,关闭网络请求 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); } //停止runloop CFRunLoopStop(CFRunLoopGetCurrent()); [self done]; } }
第二个代理方法,获取到Data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { //拼接数据 [self.imageData appendData:data]; //获取下载进度 if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf // Get the total bytes downloaded const NSInteger totalSize = self.imageData.length; // Update the data source, we must pass ALL the data, not just the new bytes //根据data获取CGImageSourceRef CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); if (width + height == 0) { //获取imageSource中的属性 CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { //初始方向 NSInteger orientationValue = -1; //获取高度 CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); //将高度copy到height中 if (val) CFNumberGetValue(val, kCFNumberLongType, &height); //获取宽度 val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); //将宽度copy到width中 if (val) CFNumberGetValue(val, kCFNumberLongType, &width); //获取方向 val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); //将方向copy到orientationValue中 if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); //释放 CFRelease(properties); // When we draw to Core Graphics, we lose orientation information, // which means the image below born of initWithCGIImage will be // oriented incorrectly sometimes. (Unlike the image born of initWithData // in connectionDidFinishLoading.) So save it here and pass it on later. orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } } //宽高都不为0 if (width + height > 0 && totalSize < self.expectedSize) { // Create the image //创建image CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE // 解决iOS平台图片失真问题 // 因为如果下载的图片是非png格式,图片会出现失真 // 为了解决这个问题,先将图片在bitmap的context下渲染 // 然后在传回partialImageRef // Workaround for iOS anamorphic image if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); //创建rgb空间 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); //获取上下文 bmContext CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); //释放rgb空间 CGColorSpaceRelease(colorSpace); if (bmContext) { //绘制图片到context中 这里的高度为partialHeight 因为height只在宽高都等于0的时候才进行的赋值,所以以后的情况下partialHeight都等于0,所以要使用当前数据(imageData)转化的图片的高度,partialImageRef为要绘制的image CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); //释放partialImageRef CGImageRelease(partialImageRef); //获取绘制的图片 partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif if (partialImageRef) { //转化image UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; //获取图片的key 其实就是url NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; //根据屏幕的大小(使用@2x或@3x) 对图片进行处理 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); } }
下载完成的代理
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { //接收数据完成后,completionBlock SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; @synchronized(self) { //线程加锁 停止runloop 当前线程置nil,连接置nil,主线程中发送异步通知 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]; }); } // 发送的request,服务器会返回一个response,就像获取服务器端的图片一样, // 如果图片没有改变,第二次获取的时候,最好直接从缓存中获取,这会省不少时间。 // response也一样,也弄一个缓存,就是NSURLCache。 // 根据你的request,看看是不是缓存中能直接获取到对应的response。 if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) { // 为NO表示没有从NSURLCache中获取到response responseFromCached = NO; } if (completionBlock) { //如果为忽略cache或从缓存中取request失败 if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { completionBlock(nil, nil, nil, YES); } else if (self.imageData) { //根据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 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]; }
发生错误的代理
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { @synchronized(self) { CFRunLoopStop(CFRunLoopGetCurrent()); self.thread = nil; self.connection = nil; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); } if (self.completedBlock) { self.completedBlock(nil, nil, error, YES); } self.completionBlock = nil; [self done]; }
// 如果我们需要对缓存做更精确的控制,我们可以实现一些代理方法来允许应用来确定请求是否应该缓存
// 如果不实现此方法,NSURLConnection 就简单地使用本来要传入 -connection:willCacheResponse: 的那个缓存对象,
所以除非你需要改变一些值或者阻止缓存,否则这个代理方法不必实现
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { // 如果该方法被调用,说明该Response不是从cache读取的,因为会会响应该方法,说明这个cacheResponse是刚从服务端获取的新鲜Response,需要进行缓存。 responseFromCached = NO; // If this method is called, it means the response wasn't read from cache if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) { // Prevents caching of responses // 如果request的缓存策略是NSURLRequestReloadIgnoringLocalCacheData,就不缓存了 return nil; } else { return cachedResponse; } }
认证相关的东东
// 当客户端向目标服务器发送请求时。服务器会使用401进行响应。客户端收到响应后便开始认证挑战(Authentication Challenge),而且是通过willSendRequestForAuthenticationChallenge:函数进行的。
// willSendRequestForAuthenticationChallenge:函数中的challenge对象包含了protectionSpace(NSURLProtectionSpace)实例属性,在此进行protectionSpace的检查。当检查不通过时既取消认证,这里需要注意下的是取消是必要的,因为willSendRequestForAuthenticationChallenge:可能会被调用多次。
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{ // NSURLProtectionSpace主要有Host、port、protocol、realm、authenticationMethod等属性。 // 为了进行认证,程序需要使用服务端期望的认证信息创建一个NSURLCredential对象。我们可以调用authenticationMethod来确定服务端的认证方法,这个认证方法是在提供的认证请求的保护空间(protectionSpace)中。 // 服务端信任认证(NSURLAuthenticationMethodServerTrust)需要一个由认证请求的保护空间提供的信任。使用credentialForTrust:来创建一个NSURLCredential对象。 if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { // SDWebImageDownloaderAllowInvalidSSLCertificates表示允许不受信任SSL认证 // 注释中提示尽量作为test使用,不要在最终production使用。 // 所以此处使用performDefaultHandlingForAuthenticationChallenge,即使用系统提供的默认行为 if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates) && [challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) { [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; } else { NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; } } else { // 每次认证失败,previousFailureCount就会加1 // 第一次认证(previousFailureCount == 0)并且有Credential,使用Credential认证 // 非第一次认证或者第一次认证没有Credential,对于认证挑战,不提供Credential就去download一个request,但是如果这里challenge是需要Credential的challenge,那么使用这个方法是徒劳的 if ([challenge previousFailureCount] == 0) { if (self.credential) { //为 challenge 的发送方提供 credential [[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge]; } else { [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } } else { [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } } }
以上为下载image 的代码,重要代码解析都在上面,这个大家应该比较熟悉,经过前几篇的介绍,这些代码的阅读应该不会很困难。
下一篇我们将做扫尾工作,将SDWebimage中的其它常用的类和方法在过一遍,这样我们对SDWebimage的理解会更加深刻。
相关文章推荐
- iOS自定义进度条
- iOS设计中字符串NSString与int及float之间的转换
- iOS毛玻璃效果(不需要任何第三方了)
- iOS 9 人机界面指南(五):图标与图形设计(附指南全文pdf下载)
- iOS的socket开发基础
- iOS 自定义进度条
- IOS应用中的数据存储
- iOS进阶——coreData的使用与学习
- iOS常用的设计模式
- iOS_SN_地图的使用(3)
- iOS项目实践之时光电影(二)
- iOS_SN_地图的使用(2)
- ios json
- 维护IOS项目时遇到的一些问题
- iOS开发之保存照片到自己创建的相簿
- iOS_SN_百度地图基本使用(1)
- ios 字典转模型
- IOS-XMPP
- IOS NSTimer和CADisplayLink的用法
- 苹果官方 Crash文件分析方法 (iOS系统Crash文件分析方法)