您的位置:首页 > 移动开发 > IOS开发

iOS----------SDWebimage源码解析(5)

2016-03-10 23:11 423 查看
本篇我们来介绍下载图片的类,创建SDWebImageDownloaderOperation对象,它继承于NSOperation,遵守SDWebImageOperation协议,下面我们来看看SDWebImageDownloaderOperation类的源码。使用SDWebImageDownloaderOperation来封装任务。

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的理解会更加深刻。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: