【原】SDWebImage源碼閱讀(四)
本文轉載請注明出處 —— polobymulberry-博客園
1. 前言
SDWebImage中主要實現了NSURLConnectionDataDelegate的以下方法:
- - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
- - (void)connectionDidFinishLoading:(NSURLConnection *)connection;
- - (nullable NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
以及NSURLConnectionDelegate的以下方法:
- - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
- - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
- - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
足足有7個函數需要實現,好多啊。具體來看看每個代理方法大概是做什么的。
2. - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
我們都知道HTTP報文是面向文本的,報文中的每一個字段都是一些ASCII碼串。HTTP有兩類報文,分別是Request和Response。HTTP的Response報文由三個部分所組成,分別是:狀態行、消息報頭、響應正文。
此處代理實現的方法中,只使用了Response的狀態碼,即statusCode。注意HTTP的statusCode小於400表示正常碼。但是304碼表示文檔的內容(自上次訪問以來或者根據請求的條件)並沒有改變,這里我們在獲取圖片時考慮直接使用Cache,所以statusCode為304時會單獨處理。
於是有了下面的框架:
if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { // ...
} else { if (code == 304) { // ... } else { // ... } }
如果Response返回正常碼,並且不為304,即if語句中的內容:
// 根據response中的expectedContentLength來給self.expectedSize進行賦值 // 而self.expectedSize此處表示響應的數據體(此處為imageData)期望大小 // 注意expectedContentLength為-1時,expectedSize賦值為0 NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; // 使用用戶自定義的progressBlock if (self.progressBlock) { self.progressBlock(0, expected); } // expected大小此處表示的就是imageData的期望大小,也就是說imageData最后下載完成大概會這么大 // 所以收到響應后,就初始化一個NSMutableData,用來存儲image數據 self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; // 不解釋,因為我發現SDWebImage只在此處使用了self.response // 應該是暴露給用戶使用的 self.response = response; // 不過好像SDWebImage中並沒有addObserver這個SDWebImageDownloadReceiveResponseNotification // 可能需要用戶自己去使用addObserver dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; });
如果response返回錯誤碼,即else中的語句:
// 獲取statusCode NSUInteger code = [((NSHTTPURLResponse *)response) statusCode]; if (code == 304) { // 當服務器端返回statusCode為“304 Not Modified”,意味着服務器端的image並沒有改變,
// 此時,我們只需取消connection,然后返回緩存中的image
// 此時返回碼是正確碼(小於400),只是不需要進行多余的connection網絡操作了,所以單獨調用
// cancelInternal [self cancelInternal]; } else { [self.connection cancel]; }
// 同SDWebImageDownloadStartNotification
dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); // 因為出錯了,所以直接調用completedBlock並返回錯誤狀態碼 if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); } // 出錯了,所以停止這個RunLoop // 我們會自然想到start函數中的CFRunLoopRun函數會結束 CFRunLoopStop(CFRunLoopGetCurrent()); // 最后在done中調用reset回收資源 並置finished為YES,executing為NO [self done];
3. - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
這一步是實實在在的獲取到了數據,第一步先將獲取到的data串到self.imageData上。因為如果image比較大的話,會多次調用didReceiveData,這樣一個image就分成很多塊了,所以每次receive到data,就串起來:
[self.imageData appendData:data];
但是我們發現這個函數總體套在一個if語句中:
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { // ... }
為什么會出現這個選項了?我覺得主要是為了單獨處理SDWebImageDownloaderProgressiveDownload,回顧一下,這個選項是在SDWebImageManager中的downloadImageWithURL中賦值的:
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
SDWebImageProgressiveDownload表示image的顯示過程是隨着下載的進度一點點進行的,而不是下載完成后,一次顯示完成。這就可以理解了,因為要隨着下載進度顯示,所以每接收到新的data,就要顯示一下。為什么還需要completedBlock呢?因為在didReceiveData中只是獲取到了imageData,但是還需要顯示在imageView上呢?那就得使用completedBlock來進行處理。所以SDWebImageProgressiveDownload默認的圖片顯示是交給用戶進行處理的。至於expectedSize為什么要大於0我就不是很清楚了。
所以在函數結尾處,我們可以看到:
dispatch_main_sync_safe(^{ if (self.completedBlock) {
// 處理此時獲得到的image
self.completedBlock(image, nil, nil, NO);
}
});
那么image是怎么產生的呢?可以看到上層包裹着一個if語句:
// partialImageRef是一個CGImageRef類型的值,本質還是self.imageData
if (partialImageRef) {
// 從CGImageRef轉化為UIImage,scale你可以理解為圖片后綴為@1x,@2x,@3x需要放大的倍數
// 至於orientation后面會講,暫時理解圖片的朝向 UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; // 有時候你不想直接把圖片的url作為cache的key,因為有可能圖片的url是動態變化的
// 所以你可以自定義一個cache key filter
// 我還沒使用過filter,所以這里一般來說就是獲得到了image的url
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// scaledImageForKey是SDWebImageCompat的一個函數,主要是根據image名稱中
// @2x,@3x來設置scale,並通過initWithCGImage來獲得image,下面會詳解 UIImage *scaledImage = [self scaledImageForKey:key image:image];
// 判斷是否要壓縮圖片,初始化默認是要壓縮圖片的 if (self.shouldDecompressImages) {
// 下面會詳解decodedImageWithImage image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; }
// 釋放資源
CGImageRelease(partialImageRef);
// 上面解釋過了 dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); }
3.1 scaledImageForKey
因為scaledImageForKey就是封裝了SDScaledImageForKey,所以我們詳解SDScaledImageForKey:
// 這是一個C++函數 inline UIImage *SDScaledImageForKey(NSString *key, UIImage *image) { // 細節考慮 if (!image) { return nil; } // 注釋中說出現這種情況的是animated images,也就是動圖 // 我們常見的是gif圖片,所以此處我們就當做gif圖片去理解
// 可以理解gif圖片是一張張靜態的圖片構成的動畫 if ([image.images count] > 0) { NSMutableArray *scaledImages = [NSMutableArray array]; // 使用了遞歸的方式,構建一組圖片動畫
for (UIImage *tempImage in image.images) { [scaledImages addObject:SDScaledImageForKey(key, tempImage)]; } // 根據這些images構成我們所需的animated image return [UIImage animatedImageWithImages:scaledImages duration:image.duration]; } else { if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) { // 比如屏幕為320x480時,scale為1,屏幕為640x960時,scale為2 CGFloat scale = [UIScreen mainScreen].scale; // “@2x.png”的長度為7,所以此處添加了這個判斷,很巧妙 if (key.length >= 8) { // 這個不用解釋了,很簡單。就是根據后綴給scale賦值 NSRange range = [key rangeOfString:@"@2x."]; if (range.location != NSNotFound) { scale = 2.0; } range = [key rangeOfString:@"@3x."]; if (range.location != NSNotFound) { scale = 3.0; } } // 使用initWithCGImage來根據Core Graphics的圖片構建UIImage。
// 這個函數可以使用scale和orientation UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; image = scaledImage; } return image; } }
3.2 decodedImageWithImage
+ (UIImage *)decodedImageWithImage:(UIImage *)image { // 當下載大量的圖片,產生內存警告時 // 自動釋放bitmap上下文環境和所有變量 // 來釋放系統內存空間 // 在iOS7中,不要忘記添加 // [[SDImageCache sharedImageCache] clearMemory]; @autoreleasepool{ // 對於animated images,不需要解壓縮 if (image.images) { return image; } CGImageRef imageRef = image.CGImage; // 感覺下面的操作就是為了將image本身的alpha去除 // 然后創建bitmap后,重新加上alpha // 圖片如果有alpha通道,就返回原始image,因為jpg圖片有alpha的話,就不壓縮 CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); if (anyAlpha) { return image; } // 圖片寬高 size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); // 圖片的ColorSpaceModel為kCGColorSpaceModelUnknown,kCGColorSpaceModelMonochrome // 和kCGColorSpaceModelIndexed時,說明該ColorSpace不受支持 bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelIndexed); // 如果屬於上述不支持的ColorSpace,ColorSpace就使用RGB if (unsupportedColorSpace) colorspaceRef = CGColorSpaceCreateDeviceRGB(); // 當你調用這個函數的時候,Quartz創建一個位圖繪制環境,也就是位圖上下文。 // 當你向上下文中繪制信息時,Quartz把你要繪制的信息作為位圖數據繪制到指定的內存塊。 // 一個新的位圖上下文的像素格式由三個參數決定: // 每個組件的位數,顏色空間,alpha選項。alpha值決定了繪制像素的透明性。 CGContextRef context = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(imageRef), 0, colorspaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); // 在上面創建的context繪制image,並以此獲取image,而該image也將擁有alpha通道 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(context); UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation]; // 開始釋放資源 if (unsupportedColorSpace) CGColorSpaceRelease(colorspaceRef); CGContextRelease(context); CGImageRelease(imageRefWithAlpha); return imageWithAlpha; } }
回到didReceiveData的剩余部分,也就是剛才那個if語句的最最外層if語句(if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock)):
// 獲取當前已經下載的數據大小 const NSInteger totalSize = self.imageData.length; // 使用最新下載后的圖片數據來創建一個CGImageSourceRef變量imageSource // 注意創建使用的數據是CoreFoundation的data,而self.imageData是NSData,所以要做如下轉化 // (__bridge CFDataRef)self.imageData CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
有了imageSource后,就要根據imageSource獲取image的各種屬性。主要是Core Graphics框架提供了很多方便的工具。所以要講imageData先轉化為CF框架下的變量,然后創建CG框架下的CGImageSource。
接着是:
// width + height == 0在此處其實就是表示width==0&&height==0 // 初始條件下,也就是第一次執行時,width和height均為0 if (width + height == 0) { // 從imageSource中獲取圖片的一些屬性,比如長寬等等,是一個dictionary變量
// 這里獲取imageSource屬性,直接傳入imageSource就行,為啥還要傳入一個index?
// 因為對於gif圖片,一個imageSource對應的CGImage會有多個,需要使用index
// 底下會使用CGImageSourceCreateImageAtIndex來根據imageSource創建一個帶index的CGImageRef CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); 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); // CoreFoundation對象類型不在ARC范圍內,所以要手動釋放資源 CFRelease(properties); // 還記得我們上面講的一段代碼,要使用Core Graphics框架繪制image // 其實就是initWithCGImage這個函數,但是使用這個函數有時候會產生 // 圖片的朝向錯誤(不像在connectionDidFinishLoading中使用initWithData所產生的image) // 所以在這里保存朝向信息,下面有些函數需要朝向信息,就傳給它 orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } }
然后就是接收到圖片數據后,width和height有值了:
// width和height更新過了,並且還沒有獲取到完整的圖片數據(totalSize < self.expectedSize) // 不過為什么獲取到完整的圖片數據就不執行了?(totalSize == self.expectedSize)
// 因為要執行connectionDidFinishLoading函數了 if (width + height > 0 && totalSize < self.expectedSize) { // ...... }
當前這個if里面有兩個if語句,第二個我們講過了,就是用completedBlock去顯示已下載的image。我們下面着重解釋第一個if
// 創建圖片 CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE // 解決iOS平台圖片失真問題 // 因為如果下載的圖片是非png格式,圖片會出現失真 // 為了解決這個問題,先將圖片在bitmap的context下渲染 // 然后在傳回partialImageRef if (partialImageRef) { // 下面代碼和decodedImageWithImage差不多 const size_t partialHeight = CGImageGetHeight(partialImageRef); 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
最后一步是調用progressBlock,我們很少見到調用progressBlock的情況,其實也跟didReceiveData這個函數有關,因為一般就是在數據量比較大的時候,需要一份一份接受數據,並拼接組裝,所以此處可以使用progressBlock。
4. - (void)connectionDidFinishLoading:(NSURLConnection *)connection
如果成功獲取服務端返回的所有數據,則代理會收到connectionDidFinishLoading:消息
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; @synchronized(self) { // 停止當前的RunLoop 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; } /*
如果options中有SDWebImageDownloaderIgnoreCachedResponse表示對應的SDWebImageOptions的options為
SDWebImageRefreshCached。而有了SDWebImageRefreshCached,就表示downloaderOptions肯定包含
SDWebImageDownloaderUseNSURLCache/SDWebImageDownloaderIgnoreCachedResponse
(大家搜一下SDWebImageRefreshCached就知道了),但是SDWebImageDownloaderUseNSURLCache和
SDWebImageDownloaderIgnoreCachedResponse又不是一定同時存在於options中。因為只有image從
SDImageCache中獲取到了才會有SDWebImageDownloaderIgnoreCachedResponse,為什么要特意提
SDImageCache?因為SDWebImage有兩種緩存方式,一個是SDImageCache,一個就是NSURLCache,所以知道
為什么這個選項是Ignore了吧,因為已經從SDImageCache獲取了image,就忽略NSURLCache了。
此處我的理解就是如果已經從SDImageCache獲取到了image,並且選項為了SDWebImageRefreshCached,就要
設置SDWebImageDownloaderIgnoreCachedResponse。我們也看到了,即使responseCached為YES了,
completedBlock的image和data參數也為nil。
我看網上對這一塊的眾說風雲,而且這一塊好像也出過不少問題,懂得大神可以私信我。好好探討一下!
我們看看這兩個選項的注釋: /** * 默認情況下,request請求使用NSURLRequestReloadIgnoringLocalCacheData作為默認策略 * 使用了這個選項,那么request使用NSURLRequestUseProtocolCachePolicy作為默認策略 */ SDWebImageDownloaderUseNSURLCache= 1 << 2 , /* * * 如果要從NSURLCache讀取image,並且還要強制刷新NSURLCache,如果有此選項后 * 就調用image和data參數為nil的completedBlock * (有該選項就一定有`SDWebImageDownloaderUseNSURLCache`). */
SDWebImageDownloaderIgnoreCachedResponse
= 1 << 3
, */ if (completionBlock) { if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { completionBlock(nil, nil, nil, YES); } else if (self.imageData) { // 因為image可能是gif,可能是webp,所以需要通過sd_imageWithData轉化為UIImage類型,具體實現后面會說 UIImage *image = [UIImage sd_imageWithData:self.imageData]; // 前面說過 NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; image = [self scaledImageForKey:key image:image]; // 注意對於gif圖片,不需要解壓縮 if (!image.images) { if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { // 圖片大小為0,報錯 completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); } else { completionBlock(image, self.imageData, nil, YES); } } else { // image為空,報錯 completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); } } // 釋放資源 self.completionBlock = nil; // 置NSConnection為完成狀態 [self done]; }
4.1 sd_imageWithData
+ (UIImage *)sd_imageWithData:(NSData *)data { // 沒有數據,細節 if (!data) { return nil; } UIImage *image; // 根據data的前面幾個字節,判斷出圖片類型,是jepg,png,gif還是...后面詳解 NSString *imageContentType = [NSData sd_contentTypeForImageData:data]; // 如果是gif圖片或webp圖片,是需要單獨處理的。后面詳解gif和webp圖片處理 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; }
4.1.1 sd_contentTypeForImageData
// NSData+ImageContentType // 每張圖片的開頭會存儲圖片的類型信息 // 很簡單的代碼,不贅述了 + (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; }
4.1.2 sd_animatedGIFWithData
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data { if (!data) { return nil; } // 根據data創建一個CG下的imageSource CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); // 返回imageSource中的image數目,為后面創建CGImage提供index size_t count = CGImageSourceGetCount(source); UIImage *animatedImage; // count<=1的時候,就當單張圖片 if (count <= 1) { animatedImage = [[UIImage alloc] initWithData:data]; } else { // 多張圖片,每幀0.1秒 NSMutableArray *images = [NSMutableArray array]; NSTimeInterval duration = 0.0f; // for (size_t i = 0; i < count; i++) { // 根據指定的index創建CGImage CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL); // 根據imageSource和指定的index獲取該CGImage的duration,后面詳解 duration += [self sd_frameDurationAtIndex:i source:source]; // 往images添加單張圖片 [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]]; CGImageRelease(image); } // 如果image中沒有duration信息,就自己計算。每幀0.1秒,算出gif動畫所需的duration if (!duration) { duration = (1.0f / 10.0f) * count; } animatedImage = [UIImage animatedImageWithImages:images duration:duration]; } // 釋放資源 CFRelease(source); return animatedImage; }
4.1.2.1 sd_frameDurationAtIndex
+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source { float frameDuration = 0.1f; // 根據imageSource和index獲取到image的屬性 CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); // 轉化CFDictionaryRef為NSDictionary NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; // 因為image是gif,所以根據kCGImagePropertyGIFDictionary獲取到image的gif的屬性 NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; // 從gifProperties根據kCGImagePropertyGIFUnclampedDelayTime獲取到該張image的duration, // 如果該gif沒有unclamped delay time,就是用kCGImagePropertyGIFDelayTime獲取delay time作為duration NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; if (delayTimeUnclampedProp) { frameDuration = [delayTimeUnclampedProp floatValue]; } else { NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; if (delayTimeProp) { frameDuration = [delayTimeProp floatValue]; } } // 許多煩人的gif的廣告,每張圖片的duration是0,這樣達到快速刷新圖片的效果 // 這里我們根據Firefox的做法,對已duration小於等於100ms的每幀圖片,指定幀率為10ms if (frameDuration < 0.011f) { frameDuration = 0.100f; } CFRelease(cfFrameProperties); return frameDuration; }
4.1.3 sd_imageWithWebPData
// WebP 是 Google 在 2010 年發布的圖片格式,希望以更高的壓縮比替代 JPEG。 // 它用 VP8 視頻幀內編碼作為其算法基礎,取得了不錯的壓縮效果。 // 它支持有損和無損壓縮、支持完整的透明通道、也支持多幀動畫,並且沒有版權問題,是一種非常理想的圖片格式。 // 借由 Google 在網絡世界的影響力,WebP 在幾年的時間內已經得到了廣泛的應用。 // 看看你手機里的 App:微博、微信、QQ、淘寶、網易新聞等等,每個 App 里都有 WebP 的身影。Facebook 則更進一步,用 WebP 來顯示聊天界面的貼紙動畫。 // WebP 標准是 Google 定制的,迄今為止也只有 Google 發布的 libwebp 實現了該的編解碼 。 所以這個庫也是該格式的事實標准。 + (UIImage *)sd_imageWithWebPData:(NSData *)data { // 具體算法我不是很清楚 // 大概就是根據data設置WebPDecoderConfig類型變量config WebPDecoderConfig config; if (!WebPInitDecoderConfig(&config)) { return nil; } if (WebPGetFeatures(data.bytes, data.length, &config.input) != VP8_STATUS_OK) { return nil; } config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB; config.options.use_threads = 1; // 注意此處又一點瑕疵,就是不支持WebP的動圖 // 此處默認是WebP的靜態圖片,所以直接使用WebPDecode //大牛們可以添加代碼,增加支持WebP動圖的功能,提示一下, // 首先用WebPDemuxer拆包,之后拆出來的單幀用WebPDecode解碼
if (WebPDecode(data.bytes, data.length, &config) != VP8_STATUS_OK) { return nil; } int width = config.input.width; int height = config.input.height; if (config.options.use_scaling) { width = config.options.scaled_width; height = config.options.scaled_height; } // 根據decode出來的rgba數組,即config.output.u.RGBA構建UIImage CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData); CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : 0; // rgba是4bytes,rgb是3bytes size_t components = config.input.has_alpha ? 4 : 3; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; // 根據provider創建image CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); CGColorSpaceRelease(colorSpaceRef); CGDataProviderRelease(provider); UIImage *image = [[UIImage alloc] initWithCGImage:imageRef]; CGImageRelease(imageRef); return image; }
4.1.4 sd_imageOrientationFromImageData
+(UIImageOrientation)sd_imageOrientationFromImageData:(NSData *)imageData { // 保證如果imageData中獲取不到朝向信息,就默認UIImageOrientationUp UIImageOrientation result = UIImageOrientationUp; CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); if (imageSource) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { CFTypeRef val; int exifOrientation; val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) { // 這個kCGImagePropertyOrientation先轉化為int值 // 然后用一個switch case語句將int轉化為朝向的enum值(sd_exifOrientationToiOSOrientation) CFNumberGetValue(val, kCFNumberIntType, &exifOrientation); result = [self sd_exifOrientationToiOSOrientation:exifOrientation]; } // else - if it's not set it remains at up CFRelease((CFTypeRef) properties); } else { //NSLog(@"NO PROPERTIES, FAIL"); } CFRelease(imageSource); } return result; }
5. - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { // 一開始的代碼和connectionDidFinishLoading代碼類似,除了少了SDWebImageDownloadFinishNotification @synchronized(self) { CFRunLoopStop(CFRunLoopGetCurrent()); self.thread = nil; self.connection = nil; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); } // 使用completedBlock報錯error if (self.completedBlock) { self.completedBlock(nil, nil, error, YES); } self.completionBlock = nil; [self done]; }
6. - (nullable NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
// 如果我們需要對緩存做更精確的控制,我們可以實現一些代理方法來允許應用來確定請求是否應該緩存 // 如果不實現此方法,NSURLConnection 就簡單地使用本來要傳入 -connection:willCacheResponse: 的那個緩存對象, // 所以除非你需要改變一些值或者阻止緩存,否則這個代理方法不必實現 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { responseFromCached = NO; // 如果該方法被調用,說明該Response不是從cache讀取的,因為會會響應該方法,說明這個cacheResponse是剛從服務端獲取的新鮮Response,需要進行緩存。 if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) { // 如果request的緩存策略是NSURLRequestReloadIgnoringLocalCacheData,就不緩存了 return nil; } else { // 否則使用默認cacheResponse return cachedResponse; } }
7. - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection
// 在構建connection會被響應。如果這個connection需要根據NSURLCredentialStorage中的權限進行構建,那么就返回YES - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection { // 默認是YES,想要修改,需要用戶自己指定self.shouldUseCredentialStorage值 return self.shouldUseCredentialStorage; }
8. - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
// 當客戶端向目標服務器發送請求時。服務器會使用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 sender] useCredential:self.credential forAuthenticationChallenge:challenge]; } else { [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } } else { [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } } }