前言:
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];
}