關于SDWebImage的一些小技巧

SDWebImage分享

網絡圖片顯示大體步驟:

1.生成一個SDWebImageCombinedOperation對象(繼承與NSOperation), 添加到operation池中
2.根據URL地址, 用MD5算出散列值存儲在本地, 根據此散列值拼接文件路徑, 檢查沙盒中是否有緩存圖片數據, 如果有, 讀出放入NSData, 然后轉換成UIImage并解碼
3.如果找不到圖片, 則啟動下載流程下載使用NSMutableURLRequest包裝的NSOperation, 放入NSOperationQueue并發下載

 dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });

下載圖片后的緩存層
下載完成后, 在給imageView對象賦值image的同時, 內部會有一個cache的控制類, 這個類會創建一個串行的GCD隊列, 執行文件寫入的操作, 而且持有一個NSCache的對象, 保留最近剛下載完成的, 并且已經解壓完成的圖片對象, 圖片在網絡層下載完成會會進行decode再往上層傳遞, 在disk里讀出圖片也是同理, 所以在兩個數據層往UI層傳遞參數的時候, 這個中間的緩存層都會引用一份已經被decode的imageData, 當下次需要使用時, 不需要再從磁盤中讀出并decode出來, 并且這個類會配合內存警告自動清空ram里的圖片緩存.

寫入磁盤
從磁盤讀取數據到內核緩沖區
從內核緩沖區復制到用戶空間(內存級別拷貝)
解壓縮為位圖(耗cpu較高)
如果位圖數據不是字節對齊的,CoreAnimation會copy一份位圖數據并進行字節對齊
CoreAnimation渲染解壓縮過的位圖
以上4,5,6,7,8步是在UIImageView的setImage時進行的,所以默認在主線程進行(iOS UI操作必須在主線程執行)

2. 一些優化思路:

同一個URL, 但是圖片改變了已被改變:

默認的行為, sd用的是圖片的url算出md5散列值存儲, 所以在同一url的情況下, 得出的散列值是一樣的, 如果圖片已經被緩存在disk上, 就會直接讀取disk上的圖而不會去刷新緩存
因此要使用SDWebImageDownloader單例里一個SDWebImageRefreshCached的策略, 并且在headersFilter中添加http的header
通過查閱HTTP協議相關的資料得知,與服務器返回的Last-Modified相對應的request header里可以加一個名為If-Modified-Since的key,value即是服務器回傳的服務端圖片最后被修改的時間,第一次圖片請求時If-Modified-Since的值為空,第二次及以后的客戶端請求會把服務器回傳的Last-Modified值作為If-Modified-Since的值傳給服務器,這樣服務器每次接收到圖片請求時就將If-Modified-Since與Last-Modified進行比較,如果客戶端圖片已陳舊那么返回狀態碼200、Last-Modified、圖片內容,客戶端存儲Last-Modified和圖片;如果客戶端圖片是最新的那么返回304 Not Modified、不會返回Last-Modified、圖片內容。

SDWebImageDownloader *imgDownloader = SDWebImageManager.sharedManager.imageDownloader;
imgDownloader.headersFilter  = ^NSDictionary *(NSURL *url, NSDictionary *headers) {

    NSFileManager *fm = [[NSFileManager alloc] init];
    NSString *imgKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
    NSString *imgPath = [SDWebImageManager.sharedManager.imageCache defaultCachePathForKey:imgKey];
    NSDictionary *fileAttr = [fm attributesOfItemAtPath:imgPath error:nil];

    NSMutableDictionary *mutableHeaders = [headers mutableCopy];

    NSDate *lastModifiedDate = nil;

    if (fileAttr.count > 0) {
        if (fileAttr.count > 0) {
            lastModifiedDate = (NSDate *)fileAttr[NSFileModificationDate];
        }

    }
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
    formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
    formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss z";

    NSString *lastModifiedStr = [formatter stringFromDate:lastModifiedDate];
    lastModifiedStr = lastModifiedStr.length > 0 ? lastModifiedStr : @"";
    [mutableHeaders setValue:lastModifiedStr forKey:@"If-Modified-Since"];

    return mutableHeaders;
};

2.1 關于異步圖片下載:

fastImageCache主要針對于從磁盤文件讀取并展示圖片的極端優化,所以并沒有集成異步圖片下載的功能。這里主要來看看SDWebImage(AFNetWorking的基本類似)的實現方案:

tableView中,異步圖片下載任務的管理:
我們知道,tableViewCell是有重用機制的,也就是說,內存中只有當前可見的cell數目的實例,滑動的時候,新顯示cell會重用被滑出的cell對象。這樣就存在一個問題:

一般情況下在我們會在cellForRow方法里面設置cell的圖片數據源,也就是說如果一個cell的imageview對象開啟了一個下載任務,這個時候該cell對象發生了重用,新的image數據源會開啟另外的一個下載任務,由于他們關聯的imageview對象實際上是同一個cell實例的imageview對象,就會發生2個下載任務回調給同一個imageview對象。這個時候就有必要做一些處理,避免回調發生時,錯誤的image數據源刷新了UI。

SDWebImage提供的UIImageView擴展的解決方案:

imageView對象會關聯一個下載列表(列表是給AnimationImages用的,這個時候會下載多張圖片),當tableview滑動,imageView重設數據源(url)時,會cancel掉下載列表中所有的任務,然后開啟一個新的下載任務。這樣子就保證了只有當前可見的cell對象的imageView對象關聯的下載任務能夠回調,不會發生image錯亂。

iOS異步任務一般有3種實現方式:
NSOperation
GCD
NSThread
SDWebImage通過自定義NSOperation來抽象下載任務的并將下載時的數據邏輯處理統一放到operation中,然后結合了GCD來做一些主線程與子線程的切換邏輯。

tableview滑動下的下載處理

從sdweb處理下載的邏輯可以知道, sd是以6個線程為最大并發數去處理下載queue, 這個跟tableview的行為其實有一定的背離. 因為當用戶滑動tableview時 (假設你的cell里需要下載一個圖片), 這是會不停地將下載的operation對象加入到隊列中, 當用戶快速滑動到列表的底部然后停下時, 需要等待隊列前面大量的任務完成后才會開始當前cell的圖片下載, 這時就需要LIFO機制了.
sd實現LIFO機制的方式十分巧妙, 使用的仍然是同一個operation并發隊列并往里添加下載的operation

if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
    // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

在一個并發隊列里, 最后進入的operation會對這個即將進入的operation添加依賴, 這樣在這個新的operation的state轉為finish之前, 這個lastOperation都不會被執行.

2.2 關于圖片解壓縮:

通用的解壓縮方案
主體的思路是在子線程,將原始的圖片渲染成一張的新的可以字節顯示的圖片,來獲取一個解壓縮過的圖片。
基本上比較流行的一些開源庫都先后支持了在異步線程完成圖片的解壓縮,并對解壓縮過后的圖片進行緩存。

這么做的優點是在setImage的時候系統省去了解碼decode的步驟,缺點就是圖片占用的空間變大。

下面的代碼是SDWebImage的解決方案:

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (image.images) {
        // Do not decode animated images
        return image;
    }

    CGImageRef imageRef = image.CGImage;
    CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize};

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

    int infoMask = (bitmapInfo & kCGBitmapAlphaInfoMask);
    BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone ||
            infoMask == kCGImageAlphaNoneSkipFirst ||
            infoMask == kCGImageAlphaNoneSkipLast);

    // CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB.
    // https://developer.apple.com/library/mac/#qa/qa1037/_index.html
    if (infoMask == kCGImageAlphaNone && CGColorSpaceGetNumberOfComponents(colorSpace) > 1) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;

        // Set noneSkipFirst.
        bitmapInfo |= kCGImageAlphaNoneSkipFirst;
    }
            // Some PNGs tell us they have alpha but only 3 components. Odd.
    else if (!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace) == 3) {
        // Unset the old alpha info.
        bitmapInfo &= ~kCGBitmapAlphaInfoMask;
        bitmapInfo |= kCGImageAlphaPremultipliedFirst;
    }

    // It calculates the bytes-per-row based on the bitsPerComponent and width arguments.
    CGContextRef context = CGBitmapContextCreate(NULL,
            imageSize.width,
            imageSize.height,
            CGImageGetBitsPerComponent(imageRef),
            0,
            colorSpace,
            bitmapInfo);
    CGColorSpaceRelease(colorSpace);

    // If failed, return undecompressed image
    if (!context) return image;

    CGContextDrawImage(context, imageRect, imageRef);
    CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);

    CGContextRelease(context);

    UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(decompressedImageRef);
    return decompressedImage;
}

2.4 關于第3,4點,內存級別拷貝

FastImageCache對這一點做了很大的優化,其他的2個開源庫則未關注這一點。這一塊木有深入研究,就引用一下FastImageCache團隊對該點的一些說明。

內存映射 平常我們讀取磁盤上的一個文件,上層API調用到最后會使用系統方法read()讀取數據,內核把磁盤數據讀入內核緩沖區,用戶再從內核緩沖區讀取數據復制到用戶內存空間,這里有一次內存拷貝的時間消耗,并且讀取后整個文件數據就已經存在于用戶內存中,占用了進程的內存空間。
FastImageCache采用了另一種讀寫文件的方法,就是用mmap把文件映射到用戶空間里的虛擬內存,文件中的位置在虛擬內存中有了對應的地址,可以像操作內存一樣操作這個文件,相當于已經把整個文件放入內存,但在真正使用到這些數據前卻不會消耗物理內存,也不會有讀寫磁盤的操作,只有真正使用這些數據時,也就是圖像準備渲染在屏幕上時,虛擬內存管理系統VMS才根據缺頁加載的機制從磁盤加載對應的數據塊到物理內存,再進行渲染。這樣的文件讀寫文件方式少了數據從內核緩存到用戶空間的拷貝,效率很高。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容