【原】SDWebImage源碼閱讀(四)


【原】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];
        }
    }
}

認證挑戰

9. 參考文章



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM