IOS SDWebImage 2.X源码阅读(三)

xiaoxiao2021-02-28  62

前言: IOS SDWebImage 2.X源码阅读(一) IOS SDWebImage 2.X源码阅读(二) IOS SDWebImage 2.X源码阅读(三) IOS SDWebImage 2.X源码阅读(四)

(5)真正到了下载图片的相关代码了………………

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];

看下NSMutableURLRequest相关的概念 NSURLRequestCachePolicy缓存策略的枚举值,相关概念参考

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy) { NSURLRequestUseProtocolCachePolicy = 0,//对特定的 URL 请求使用网络协议中实现的缓存逻辑。这是默认的策略。------默认行为 NSURLRequestReloadIgnoringLocalCacheData = 1,//数据需要从原始地址加载。不使用现有缓存。------不使用缓存 NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存(未实现) NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData, NSURLRequestReturnCacheDataElseLoad = 2,//无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。------使用缓存(不管它是否过期),如果缓存中没有,那从网络加载吧 NSURLRequestReturnCacheDataDontLoad = 3,//无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。------离线模式:使用缓存(不管它是否过期),但是不从网络加载 NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。(未实现) }; /*YES:URL loading system会自动为NSURLRequest发送合适的存储cookie。从NSURLResponse返回的cookie也会根据当前的cookie访问策略(cookie acceptance policy)接收到系统中。 NO:不使用cookie */ request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); /* HTTPShouldUsePipelining表示receiver(理解为iOS客户端)的下一个信息是否必须等到上一个请求回复才能发送。 如果为YES表示可以,NO表示必须等receiver收到先前的回复才能发送下个信息。 */ request.HTTPShouldUsePipelining = YES; /* #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类中的initWithRequest方法,网络请求数据,下载图片,这个等下看,先看下面的operation属性设置

//解压下载和缓存的图像可以提高性能,但会占用大量内存。 operation.shouldDecompressImages = wself.shouldDecompressImages;//yes

web 服务可以在返回 http 响应时附带认证要求的challenge,作用是询问 http 请求的发起方是谁,这时发起方应提供正确的用户名和密码(即认证信息),然后 web 服务才会返回真正的 http 响应。

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; }

将operation 实例加入到 NSOperationQueue 中,就会调用start(),等会看start方法

[wself.downloadQueue addOperation:operation];

NSOperation之间可以设置依赖来保证执行顺序,⽐如一定要让操作A执行完后,才能执行操作B,可以像下面这么写

[operationB addDependency:operationA]; // 操作B依赖于操作A

if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {//下载操作以堆栈样式执行(后进先出)。 [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; }

看下SDWebImageDownloaderOperation类中的下载图片的方法,都是初始化的一些方法

– (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock;

NSOperation是个抽象类,并不具备分装操作的能力,必须使用它的子类 1、NSInvocationOperation 2、NSBlockOperation 3、自定义子类继承自NSOperation,实现内部相应的方法 下载图片的SDWebImageDownloaderOperation就是自定义继承NSOperation类,并实现了相关的方法,关于自定义NSOPeration的可查看该文章

/* *自定义并发的NSOperation需要以下步骤: 1.start方法:该方法必须实现, 2.main:该方法可选,如果你在start方法中定义了你的任务,则这个方法就可以不实现,但通常为了代码逻辑清晰,通常会在该方法中定义自己的任务 3.isExecuting isFinished 主要作用是在线程状态改变时,产生适当的KVO通知 4.isConcurrent :必须覆盖并返回YES; */ - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } - (BOOL)isConcurrent { return YES; }

现在来看下start()方法,一进去该方法就是一个线程线程同步锁,检查当前的operation是否取消了,若是取消了,标示当前任务已完成,并将相关的信息reset,相关的都置nil,然后初始化一个NSURLConnection

@synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; }

然后是当app进入后台之后,该如何操作

#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

1、 NSClassFromString():根据字符串名称获取同名的类 2、 [self shouldContinueWhenAppEntersBackground]:SDWebImageDownloaderOperation设置SDWebImageContinueInBackground属性 3、 beginBackgroundTaskWithExpirationHandler后面的block,会在app进入后台之后,如果在系统规定时间内任务还没有完成,在时间到之前会调用到这个方法 4、必须调用endBackgroundTask:方法来结束使用beginBackgroundTaskWithExpirationHandler:方法开始的任务。 如果你不这样做,系统可能会终止你的应用程序。 该方法可以安全地在非主线程上调用。 5、若是在指定的时间内任务未完成,先调用cancle方法

@synchronized (self) { //……………………………………………………………… self.executing = YES; //ios7以后就不再使用NSURLConnection,使用NSURLSession代替,我们现在的版本是3.X /* 第一个参数:请求对象 第二个参数:谁成为NSURLConnetion对象的代理 第三个参数:是否马上发送网络请求,如果该值为YES则立刻发送,如果为NO则不会发送网路请求 设置回调方法也在子线程中运行,在子线程中默认是没有runloop,需添加一个 RunLoop 到当前的线程中来, */ self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread]; }

执行[connection start]开始网络请求

//在startImmediately为NO时,调用该方法控制网络请求的发送 [self.connection start];

由于我们是在子线程中运行,那么它调用的委托方法也是在对应的子线程中执行,其委托方法会不断接收到下载的数据,为了防止子线程被kill掉,在该子线程中添加一个runloop,让该线程一直在等待下载结束,被手动kill

AppLog(@"开始下载前 runloop before"); if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); } else { CFRunLoopRun(); } AppLog(@"下载完成 runloop after");

看下NSURLConnectionDataDelegate的几个方法

/* 1.当接收到服务器响应的时候调用,该方法只会调用一次 第一个参数connection:监听的是哪个NSURLConnection对象 第二个参数response:接收到的服务器返回的响应头信息 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { //........ }

看下该方法中的相关代码,首先判断响应头中的状态码

//是否实现了statusCode方法 || (服务器返回的响应头的状态码 < 400 && statusCode !=304 ) if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) //预计接收的数据大小,调用下载进度的block NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; if (self.progressBlock) { self.progressBlock(0, expected); } //初始化下载数据 self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; self.response = response; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; });

看下刚才响应头的statusCode的else情况的代码,即我们正常情况下不成功的情况

NSUInteger code = [((NSHTTPURLResponse *)response) statusCode]; if (code == 304) {//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());//手动停止当前runloop,退出去 [self done];

继续看下NSURLConnectionDataDelegate委托方法connection:didReceiveData:data,该方法是多次调用,直到数据下载完成

/* 2.当接收到数据的时候调用,该方法会被调用多次 第一个参数connection:监听的是哪个NSURLConnection对象 第二个参数data:本次接收到的服务端返回的二进制数据(可能是片段) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { //......... }

将请求到的数据添加到self.imageData变量中

[self.imageData appendData:data];

下载设置了SDWebImageDownloaderProgressiveDownload属性 && 下载数据的预计大小 > 0 && self.completedBlock

if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {}

看下上面if里面的代码

//当前接收到数据的大小 const NSInteger totalSize = self.imageData.length; //更新数据源,我们必须传递所有的数据,而不仅仅是新的字节 CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); //第一次进来时,会执行如下 if (width + height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);//从source里面读取各个图片放入数组里面。 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绘制image时,我们会失去了图片方向的信息,这意味着initWithCGIImage所生成的下面的图像有时会被错误地定位。 (与connectionDidFinishLoading中的initWithData所生成的图像不同。)因此,将图片的方向信息保存在此处并稍后传递。 orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } }

然后图片的width 和 height获取到值了

if (width + height > 0 && totalSize < self.expectedSize) 图片宽高 > 0 && 当前下载数据大小 < 预计数据大小

该if条件下有两个if判断,我们先看第一个在,iOS

// Create the image CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE // Workaround for iOS anamorphic image iOS变形图像的解决方法 if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); //RGBA 色彩 (显示3色) 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判断

if (partialImageRef) { UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; AppLog(@"connection key-->%@",key); //根据key从内存缓存中查找图片 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) { //此时图片还未下载完成,所以为no self.completedBlock(image, nil, nil, NO); } }); }

继续调用用户自定义的progressBlock

if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); }

继续看下NSURLConnectionDataDelegate委托方法connectionDidFinishLoading,该方法是在服务端返回的数据接收完毕之后会调用

/* 3.当服务端返回的数据接收完毕之后会调用 通常在该方法中解析服务器返回的数据 */ - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { AppLog(@"数据请求完成!connectionDidFinish") //加锁 @synchronized(self) { CFRunLoopStop(CFRunLoopGetCurrent());//手动停止runloop //回收资源 self.thread = nil; self.connection = nil; } } //发送的request,服务器会返回一个响应的response,我们加载图片时,如果图片没有改变,可以直接从缓存中获取,其实response也是一样的,也有个NSURLCache,根据request,看看是不是命中缓存 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]; AppLog(@"key(SDWebImageDownloaderOperation)-->%@",key); AppLog(@"对图片进行相关缩放处理"); image = [self scaledImageForKey:key image:image];//对图片进行相关缩放处理 //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]; }

看下对下载图片的相关处理,在上面的方法中,有调用了sd_imageWithData:data方法

+ (UIImage *)sd_imageWithData:(NSData *)data { if (!data) { return nil; } UIImage *image; //根据NSData的前几个字节就能判断图片的类型,jpeg,png,gif,tiff,webp NSString *imageContentType = [NSData sd_contentTypeForImageData:data];//图片类型 AppLog(@"imageContentType-->%@",imageContentType); if ([imageContentType isEqualToString:@"image/gif"]) { image = [UIImage sd_animatedGIFWithData:data]; } #ifdef SD_WEBP else if ([imageContentType isEqualToString:@"image/webp"]) { image = [UIImage sd_imageWithWebPData:data]; } #endif else { image = [[UIImage alloc] initWithData:data]; UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];//图片方向 if (orientation != UIImageOrientationUp) { image = [UIImage imageWithCGImage:image.CGImage scale:image.scale orientation:orientation]; } } return image; }

根据NSData的前几个字节就能判断图片的类型,jpeg,png,gif,tiff,webp等 但是在这个版本的SDWebImage中,没有对webp类型的图片,进行处理

+ (NSString *)sd_contentTypeForImageData:(NSData *)data { uint8_t c; [data getBytes:&c length:1]; switch (c) { case 0xFF: return @"image/jpeg"; case 0x89: return @"image/png"; case 0x47: return @"image/gif"; case 0x49: case 0x4D: return @"image/tiff"; case 0x52: // R as RIFF for WEBP if ([data length] < 12) { return nil; } NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { return @"image/webp"; } return nil; } return nil; }

上面是对connection连接成功的相关处理,现在看下连接失败的delegate的方法

//连接失败 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { AppLog(@"连接失败"); AppLog(@"error-->%@",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]; }
转载请注明原文地址: https://www.6miu.com/read-2624107.html

最新回复(0)