iOS----------SDWebimage原始碼解析(5)
本篇我們來介紹下載圖片的類,建立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的理解會更加深刻。