關(guān)于SDWebImage的一些小技巧

SDWebImage分享

網(wǎng)絡(luò)圖片顯示大體步驟:

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

 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對(duì)象賦值image的同時(shí), 內(nèi)部會(huì)有一個(gè)cache的控制類(lèi), 這個(gè)類(lèi)會(huì)創(chuàng)建一個(gè)串行的GCD隊(duì)列, 執(zhí)行文件寫(xiě)入的操作, 而且持有一個(gè)NSCache的對(duì)象, 保留最近剛下載完成的, 并且已經(jīng)解壓完成的圖片對(duì)象, 圖片在網(wǎng)絡(luò)層下載完成會(huì)會(huì)進(jìn)行decode再往上層傳遞, 在disk里讀出圖片也是同理, 所以在兩個(gè)數(shù)據(jù)層往UI層傳遞參數(shù)的時(shí)候, 這個(gè)中間的緩存層都會(huì)引用一份已經(jīng)被decode的imageData, 當(dāng)下次需要使用時(shí), 不需要再?gòu)拇疟P(pán)中讀出并decode出來(lái), 并且這個(gè)類(lèi)會(huì)配合內(nèi)存警告自動(dòng)清空ram里的圖片緩存.

寫(xiě)入磁盤(pán)
從磁盤(pán)讀取數(shù)據(jù)到內(nèi)核緩沖區(qū)
從內(nèi)核緩沖區(qū)復(fù)制到用戶(hù)空間(內(nèi)存級(jí)別拷貝)
解壓縮為位圖(耗cpu較高)
如果位圖數(shù)據(jù)不是字節(jié)對(duì)齊的,CoreAnimation會(huì)copy一份位圖數(shù)據(jù)并進(jìn)行字節(jié)對(duì)齊
CoreAnimation渲染解壓縮過(guò)的位圖
以上4,5,6,7,8步是在UIImageView的setImage時(shí)進(jìn)行的,所以默認(rèn)在主線程進(jìn)行(iOS UI操作必須在主線程執(zhí)行)

2. 一些優(yōu)化思路:

同一個(gè)URL, 但是圖片改變了已被改變:

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

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 關(guān)于異步圖片下載:

fastImageCache主要針對(duì)于從磁盤(pán)文件讀取并展示圖片的極端優(yōu)化,所以并沒(méi)有集成異步圖片下載的功能。這里主要來(lái)看看SDWebImage(AFNetWorking的基本類(lèi)似)的實(shí)現(xiàn)方案:

tableView中,異步圖片下載任務(wù)的管理:
我們知道,tableViewCell是有重用機(jī)制的,也就是說(shuō),內(nèi)存中只有當(dāng)前可見(jiàn)的cell數(shù)目的實(shí)例,滑動(dòng)的時(shí)候,新顯示cell會(huì)重用被滑出的cell對(duì)象。這樣就存在一個(gè)問(wèn)題:

一般情況下在我們會(huì)在cellForRow方法里面設(shè)置cell的圖片數(shù)據(jù)源,也就是說(shuō)如果一個(gè)cell的imageview對(duì)象開(kāi)啟了一個(gè)下載任務(wù),這個(gè)時(shí)候該cell對(duì)象發(fā)生了重用,新的image數(shù)據(jù)源會(huì)開(kāi)啟另外的一個(gè)下載任務(wù),由于他們關(guān)聯(lián)的imageview對(duì)象實(shí)際上是同一個(gè)cell實(shí)例的imageview對(duì)象,就會(huì)發(fā)生2個(gè)下載任務(wù)回調(diào)給同一個(gè)imageview對(duì)象。這個(gè)時(shí)候就有必要做一些處理,避免回調(diào)發(fā)生時(shí),錯(cuò)誤的image數(shù)據(jù)源刷新了UI。

SDWebImage提供的UIImageView擴(kuò)展的解決方案:

imageView對(duì)象會(huì)關(guān)聯(lián)一個(gè)下載列表(列表是給AnimationImages用的,這個(gè)時(shí)候會(huì)下載多張圖片),當(dāng)tableview滑動(dòng),imageView重設(shè)數(shù)據(jù)源(url)時(shí),會(huì)cancel掉下載列表中所有的任務(wù),然后開(kāi)啟一個(gè)新的下載任務(wù)。這樣子就保證了只有當(dāng)前可見(jiàn)的cell對(duì)象的imageView對(duì)象關(guān)聯(lián)的下載任務(wù)能夠回調(diào),不會(huì)發(fā)生image錯(cuò)亂。

iOS異步任務(wù)一般有3種實(shí)現(xiàn)方式:
NSOperation
GCD
NSThread
SDWebImage通過(guò)自定義NSOperation來(lái)抽象下載任務(wù)的并將下載時(shí)的數(shù)據(jù)邏輯處理統(tǒng)一放到operation中,然后結(jié)合了GCD來(lái)做一些主線程與子線程的切換邏輯。

tableview滑動(dòng)下的下載處理

從sdweb處理下載的邏輯可以知道, sd是以6個(gè)線程為最大并發(fā)數(shù)去處理下載queue, 這個(gè)跟tableview的行為其實(shí)有一定的背離. 因?yàn)楫?dāng)用戶(hù)滑動(dòng)tableview時(shí) (假設(shè)你的cell里需要下載一個(gè)圖片), 這是會(huì)不停地將下載的operation對(duì)象加入到隊(duì)列中, 當(dāng)用戶(hù)快速滑動(dòng)到列表的底部然后停下時(shí), 需要等待隊(duì)列前面大量的任務(wù)完成后才會(huì)開(kāi)始當(dāng)前cell的圖片下載, 這時(shí)就需要LIFO機(jī)制了.
sd實(shí)現(xiàn)LIFO機(jī)制的方式十分巧妙, 使用的仍然是同一個(gè)operation并發(fā)隊(duì)列并往里添加下載的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;
}

在一個(gè)并發(fā)隊(duì)列里, 最后進(jìn)入的operation會(huì)對(duì)這個(gè)即將進(jìn)入的operation添加依賴(lài), 這樣在這個(gè)新的operation的state轉(zhuǎn)為finish之前, 這個(gè)lastOperation都不會(huì)被執(zhí)行.

2.2 關(guān)于圖片解壓縮:

通用的解壓縮方案
主體的思路是在子線程,將原始的圖片渲染成一張的新的可以字節(jié)顯示的圖片,來(lái)獲取一個(gè)解壓縮過(guò)的圖片。
基本上比較流行的一些開(kāi)源庫(kù)都先后支持了在異步線程完成圖片的解壓縮,并對(duì)解壓縮過(guò)后的圖片進(jìn)行緩存。

這么做的優(yōu)點(diǎn)是在setImage的時(shí)候系統(tǒng)省去了解碼decode的步驟,缺點(diǎn)就是圖片占用的空間變大。

下面的代碼是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 關(guān)于第3,4點(diǎn),內(nèi)存級(jí)別拷貝

FastImageCache對(duì)這一點(diǎn)做了很大的優(yōu)化,其他的2個(gè)開(kāi)源庫(kù)則未關(guān)注這一點(diǎn)。這一塊木有深入研究,就引用一下FastImageCache團(tuán)隊(duì)對(duì)該點(diǎn)的一些說(shuō)明。

內(nèi)存映射 平常我們讀取磁盤(pán)上的一個(gè)文件,上層API調(diào)用到最后會(huì)使用系統(tǒng)方法read()讀取數(shù)據(jù),內(nèi)核把磁盤(pán)數(shù)據(jù)讀入內(nèi)核緩沖區(qū),用戶(hù)再?gòu)膬?nèi)核緩沖區(qū)讀取數(shù)據(jù)復(fù)制到用戶(hù)內(nèi)存空間,這里有一次內(nèi)存拷貝的時(shí)間消耗,并且讀取后整個(gè)文件數(shù)據(jù)就已經(jīng)存在于用戶(hù)內(nèi)存中,占用了進(jìn)程的內(nèi)存空間。
FastImageCache采用了另一種讀寫(xiě)文件的方法,就是用mmap把文件映射到用戶(hù)空間里的虛擬內(nèi)存,文件中的位置在虛擬內(nèi)存中有了對(duì)應(yīng)的地址,可以像操作內(nèi)存一樣操作這個(gè)文件,相當(dāng)于已經(jīng)把整個(gè)文件放入內(nèi)存,但在真正使用到這些數(shù)據(jù)前卻不會(huì)消耗物理內(nèi)存,也不會(huì)有讀寫(xiě)磁盤(pán)的操作,只有真正使用這些數(shù)據(jù)時(shí),也就是圖像準(zhǔn)備渲染在屏幕上時(shí),虛擬內(nèi)存管理系統(tǒng)VMS才根據(jù)缺頁(yè)加載的機(jī)制從磁盤(pán)加載對(duì)應(yīng)的數(shù)據(jù)塊到物理內(nèi)存,再進(jìn)行渲染。這樣的文件讀寫(xiě)文件方式少了數(shù)據(jù)從內(nèi)核緩存到用戶(hù)空間的拷貝,效率很高。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容