SDWebImage源碼解析<二>

前言

我們在第一篇文章《SDWebImage源碼解析<一>》已經(jīng)了解到SDWebImage是通過 SDWebImageManager 類進(jìn)行協(xié)調(diào),調(diào)用 SDImageCacheSDWebImageDownloader 來實現(xiàn)圖片的緩存查詢與網(wǎng)絡(luò)下載的。今天我們就來分析一下SDImageCacheSDWebImageDownloader

SDImageCache

該類維護(hù)了一個內(nèi)存緩存與一個可選的磁盤緩存。同時菊卷,磁盤緩存的寫操作是異步的煤率,所以它不會對 UI 造成不必要的影響嗜闻。

存儲圖片

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // 內(nèi)存緩存 前提是設(shè)置了需要進(jìn)行秘车,將其存入 NSCache 中典勇,同時傳入圖片的消耗值,cost 為像素值(當(dāng)內(nèi)存受限或者所有緩存對象的總代價超過了最大允許的值時叮趴,緩存會移除其中的一些對象)
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    // 磁盤緩存
    if (toDisk) {
        // 將緩存操作作為一個任務(wù)放入ioQueue中異步執(zhí)行
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;

            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                // 需要確定圖片是PNG還是JPEG割笙。PNG圖片容易檢測,因為有一個唯一簽名眯亦。PNG圖像的前8個字節(jié)總是包含以下值:137 80 78 71 13 10 26 10
                // 在imageData為nil的情況下假定圖像為PNG伤溉。我們將其當(dāng)作PNG以避免丟失透明度。
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;
                // 而當(dāng)有圖片數(shù)據(jù)時搔驼,我們檢測其前綴谈火,確定圖片的類型
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
               // 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 將其轉(zhuǎn)化為 NSData舌涨,否則按照 JPEG 格式轉(zhuǎn)化糯耍,并且壓縮質(zhì)量為 1,即無壓縮
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
#else
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
            }
           // 創(chuàng)建緩存文件并存儲圖片(使用 fileManager)
            if (data) {
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                // 根據(jù)image的key獲取緩存路徑
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                // 不適用iCloud備份
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

查詢圖片

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    // 對doneBlock囊嘉、key判空 查找內(nèi)存緩存
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 首先檢查內(nèi)存緩存(查詢是同步的)温技,如果查找到,則直接回調(diào) doneBlock 并返回

    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) { // isCancelled初始默認(rèn)值為NO
            return;
        }

        @autoreleasepool {
// 檢查磁盤緩存(查詢是異步的)扭粱,如果查找到舵鳞,則將其放到內(nèi)存緩存,并調(diào)用 doneBlock 回調(diào)
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
// 緩存至內(nèi)存(NSCache)中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
// 返回主線程設(shè)置圖片
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

通過代碼可以看到operation雖然沒有具體的內(nèi)容琢蛤,但是我們可以在外部調(diào)用operation的cancel方法來改變isCancelled的值蜓堕。這樣做對從內(nèi)存緩存中查找到圖片的本次操作查詢過程沒有影響,但是如果本次查詢過程是在磁盤緩存中進(jìn)行的博其,就會受到影響套才,autoreleasepool{}代碼塊不再執(zhí)行。而在這段代碼塊完成了這樣的工作:將磁盤緩存取出進(jìn)行內(nèi)存緩存慕淡,在線程執(zhí)行完成回調(diào)背伴。因此可以看到這個返回的NSOpeation值可以幫助我們在外部控制不再進(jìn)行磁盤緩存查詢和內(nèi)存緩存?zhèn)浞莸牟僮鳎瑲w根結(jié)底就是向外部暴漏了取消操作的接口峰髓。

清除圖片

對于清理方法cleanDiskWithCompletionBlock:傻寂,有兩個條件:文件的緩存有效期及最大緩存空間大小。

  • 文件的緩存有效期可以通過maxCacheAge屬性來設(shè)置携兵,默認(rèn)是1周的時間疾掰。如果文件的緩存時間超過這個時間值,則將其移除徐紧。
  • 最大緩存空間大小是通過maxCacheSize屬性來設(shè)置的个绍,如果所有緩存文件的總大小超過這一大小勒葱,則會按照文件最后修改時間的逆序,以每次一半的遞歸來移除那些過早的文件巴柿,直到緩存的實際大小小于我們設(shè)置的最大使用空間凛虽。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // 枚舉器預(yù)先獲取緩存文件的有用的屬性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // 枚舉緩存文件夾中所有文件,該迭代有兩個目的:移除比過期日期更老的文件广恢;存儲文件屬性以備后面執(zhí)行基于緩存大小的清理操作
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
            // 跳過文件夾
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // 移除早于有效期的老文件
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // 存儲文件的引用并計算所有文件的總大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果磁盤緩存的大小超過我們配置的最大大小凯旋,則執(zhí)行基于文件大小的清理,我們首先刪除最老的文件
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
            // 以設(shè)置的最大緩存大小的一半值作為清理目標(biāo)
            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            // 按照最后修改時間來排序剩下的緩存文件
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            // 刪除文件钉迷,直到緩存總大小降到我們期望的大小
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

SDWebImageDownloaderOptions

在下載的過程中至非,程序會根據(jù)設(shè)置的不同的下載選項,而執(zhí)行不同的操作糠聪。下載選項由枚舉 SDWebImageDownloaderOptions定義荒椭,具體如下:

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    /// 漸進(jìn)式下載,如果設(shè)置了這個選項舰蟆,會在下載過程中趣惠,每次接收到一段chunk數(shù)據(jù)就調(diào)用一次完成回調(diào)(注意是完成回調(diào))回調(diào)中的image參數(shù)為未下載完成的部分圖像
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /// 通常情況下request阻止使用NSURLCache. 這個選項會用默認(rèn)策略使用NSURLCache 
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /// 如果從NSURLCache中讀取圖片,會在調(diào)用完成block時身害,傳遞空的image或imageData \
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    /// 系統(tǒng)為iOS 4+時味悄,如果應(yīng)用進(jìn)入后臺,繼續(xù)下載塌鸯。這個選項是為了實現(xiàn)在后臺申請額外的時間來完成請求侍瑟。如果后臺任務(wù)到期,操作會被取消丙猬。
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /// 通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式來處理存儲在NSHTTPCookieStore的cookies
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /// 允許不受信任的SSL證書涨颜,在測試環(huán)境中很有用,在生產(chǎn)環(huán)境中要謹(jǐn)慎使用
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /// 將圖片下載放到高優(yōu)先級隊列中
    SDWebImageDownloaderHighPriority = 1 << 7,
};

下面我們看一下SDWebImageDownloaderOperation對NSOperation的-start方法的重寫茧球,畢竟這是完成下載任務(wù)的核心代碼

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
// 將各個屬性置空庭瑰。包括取消回調(diào)、完成回調(diào)袜腥、進(jìn)度回調(diào)见擦,用于網(wǎng)絡(luò)連接的connection钉汗,用于拼接數(shù)據(jù)的imageData羹令、記錄當(dāng)前線程的屬性thread。
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系統(tǒng)借用一點時間损痰,繼續(xù)執(zhí)行下面的代碼來完成connection的創(chuàng)建和進(jìn)行下載任務(wù)福侈。
 // 在后臺任務(wù)執(zhí)行時間超過最大時間時,也就是后臺任務(wù)過期執(zhí)行過期回調(diào)卢未。在回調(diào)主動將這個后臺任務(wù)結(jié)束肪凛。
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  Create the session for this task
             *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
             *  method calls and completion handler calls.
             */
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;// 標(biāo)記狀態(tài)
        self.thread = [NSThread currentThread]; // 記錄當(dāng)前線程
    }
    
    [self.dataTask resume];

    if (self.dataTask) {
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    }
    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
}

SDWebImageDownloader

SDWebImageDownloader有一個重要的屬性executionOrder代表著下載操作執(zhí)行的順序堰汉,它是一個SDWebImageDownloaderExecutionOrder枚舉類型

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    // 默認(rèn)值,所有的下載操作以隊列類型 (先進(jìn)先出)執(zhí)行.
    SDWebImageDownloaderFIFOExecutionOrder,

    // 所有的下載操作以棧類型 (后進(jìn)先出)執(zhí)行.
    SDWebImageDownloaderLIFOExecutionOrder
};

默認(rèn)是SDWebImageDownloaderFIFOExecutionOrder伟墙,是在init方法中設(shè)置的翘鸭。如果設(shè)置了后進(jìn)先出,在下載操作添加到下載隊列中時戳葵,會依據(jù)這個值添加依賴關(guān)系就乓,使得最后添加操作出在依賴關(guān)系鏈條中的第一項,因而會優(yōu)先下載最后添加的操作任務(wù)拱烁。
SDWebImageDownloader還提供了其他幾個重要的對外接口(包括屬性和方法):
1.BOOL shouldDecompressImages
是否需要解壓生蚁,在init中設(shè)置默認(rèn)值為YES,在下載操作創(chuàng)建之后將值傳遞給操作的同名屬性戏自。
解壓下載或緩存的圖片可以提升性能邦投,但是會消耗很多內(nèi)存
默認(rèn)是YES,如果你會遇到因為過高的內(nèi)存消耗引起的崩潰將它設(shè)置為NO擅笔。
2.NSInteger maxConcurrentDownloads
放到下載隊列中的下載操作的總數(shù)志衣,是一個瞬間值,因為下載操作一旦執(zhí)行完成剂娄,就會從隊列中移除蠢涝。
3.NSUInteger currentDownloadCount
下載操作的超時時長默認(rèn)是15.0,即request的超時時長阅懦,若設(shè)置為0和二,在創(chuàng)建request的時候依然使用15.0。
只讀耳胎。
4.NSURLCredential *urlCredential
為request操作設(shè)置默認(rèn)的URL憑據(jù)惯吕,具體實施為:在將操作添加到隊列之前,將操作的credential屬性值設(shè)置為urlCredential
5.NSString *username和NSString *passwords
如果設(shè)置了用戶名和密碼:在將操作添加到隊列之前怕午,會將操作的credential屬性值設(shè)置為[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]废登,而忽略了屬性值urlCredential。
6.- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
為HTTP header設(shè)置value郁惜,用來追加到每個下載對應(yīng)的HTTP request, 若傳遞的value為nil堡距,則將對應(yīng)的field移除。
擴(kuò)展里面定義了一個HTTPHeaders屬性(NSMutableDictionary類型)用來存儲所有設(shè)置好的header和對應(yīng)value兆蕉。
在創(chuàng)建request之后緊接著會將HTTPHeaders賦給request羽戒,request.allHTTPHeaderFields = self.HTTPHeaders;
7.- (NSString *)valueForHTTPHeaderField:(NSString *)field;
返回指定的HTTP header field對應(yīng)的value
8.SDWebImageDownloaderHeadersFilterBlock headersFilter
設(shè)置一個過濾器,為下載圖片的HTTP request選取header.意味著最終使用的headers是經(jīng)過這個block過濾之后的返回值虎韵。
9.- (void)setOperationClass:(Class)operationClass;
設(shè)置一個SDWebImageDownloaderOperation的子類 易稠,在每次 SDWebImage 構(gòu)建一個下載圖片的請求操作的時候作為默認(rèn)的NSOperation使用.
參數(shù)operationClass為要設(shè)置的默認(rèn)下載操作的SDWebImageDownloaderOperation的子類。 傳遞 nil 會恢復(fù)為SDWebImageDownloaderOperation包蓝。
以下兩個方法是下載控制方法了
- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:
這個方法用指定的URL創(chuàng)建一個異步下載實例驶社。
有關(guān)completedBlock回調(diào)的一些解釋:下載完成的時候block會調(diào)用一次.
沒有使用SDWebImageDownloaderProgressiveDownload選項的情況下企量,如果下載成功會設(shè)置image參數(shù),如果出錯,會根據(jù)錯誤設(shè)置error參數(shù). 最后一個參數(shù)總是YES. 如果使用了SDWebImageDownloaderProgressiveDownload選項亡电,這個block會使用部分image的對象有間隔地重復(fù)調(diào)用届巩,同時finished參數(shù)設(shè)置為NO,直到使用完整的image對象和值為YES的finished參數(shù)進(jìn)行最后一次調(diào)用.如果出錯,finished參數(shù)總是YES.
- (void)setSuspended:(BOOL)suspended;
設(shè)置下載隊列的掛起(暫停)狀態(tài)份乒。若為YES姆泻,隊列不再開啟新的下載操作,再向隊列里面添加的操作也不會被開啟冒嫡,但是正在執(zhí)行的操作依然繼續(xù)執(zhí)行拇勃。

下面我們就來看一下下載方法的實現(xiàn)細(xì)節(jié):

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;

    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

       // 創(chuàng)建請求對象,并根據(jù) options 參數(shù)設(shè)置其屬性  
    // 為了避免潛在的重復(fù)緩存(NSURLCache + SDImageCache)孝凌,如果沒有明確告知需要緩存方咆,則禁用圖片請求的緩存操作 
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
// 創(chuàng)建 SDWebImageDownloaderOperation 操作對象,傳入進(jìn)度回調(diào)蟀架、完成回調(diào)瓣赂、取消回調(diào)  
    // 配置信息包括是否需要認(rèn)證、優(yōu)先級
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                        inSession:self.session
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 從管理器的 callbacksForURL 中找出該 URL 所有的進(jìn)度處理回調(diào)并調(diào)用
// 將刪除所有回調(diào)的block放到隊列barrierQueue中使用barrier_sync方式執(zhí)行片拍,確保了在進(jìn)行調(diào)用完成回調(diào)之前所有的使用url對應(yīng)的回調(diào)的地方都是正確的數(shù)據(jù)煌集。
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 從管理器的 callbacksForURL 中找出該 URL 所有的完成處理回調(diào)并調(diào)用  
                               // 如果 finished 為 YES,則將該 url 對應(yīng)的回調(diào)信息從 URLCallbacks 中刪除 
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            __block NSArray *callbacksForURL;
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
// 取消操作將該 url 對應(yīng)的回調(diào)信息從 URLCallbacks 中刪除 
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
// 設(shè)置是否需要解壓
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        // 設(shè)置進(jìn)行網(wǎng)絡(luò)訪問驗證的憑據(jù)
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        // 根據(jù)下載選項SDWebImageDownloaderHighPriority設(shè)置優(yōu)先級
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
// 將操作加入到操作隊列 downloadQueue 中  
    // 如果是 LIFO 順序捌省,則將新的操作作為原隊列中最后一個操作的依賴苫纤,然后將新操作設(shè)置為最后一個操作 
        [wself.downloadQueue addOperation:operation];
// 根據(jù)executionOrder設(shè)置操作的依賴關(guān)系
        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;
        }
    }];

    return operation;
}

重點就是addProgressCallback: completedBlock: forURL: createCallback:的執(zhí)行了,SDWebImageDownloader將外部傳來的進(jìn)度回調(diào)纲缓、完成回調(diào)卷拘、url直接傳遞給這個方法,并實現(xiàn)創(chuàng)建下載操作的代碼塊作為這個方法的createCallback參數(shù)值祝高。下面就看一下這個方法的實現(xiàn)細(xì)節(jié):

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // 對URL判空栗弟,如果為空,直接執(zhí)行完成回調(diào)工闺。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    /*
    對dispatch_barrier_sync函數(shù)的解釋:
     向分配隊列提交一個同步執(zhí)行的barrier block乍赫。與dispatch_barrier_async不同,這個函數(shù)直到barrier block執(zhí)行完畢才會返回陆蟆,在當(dāng)前隊列調(diào)用這個函數(shù)會導(dǎo)致死鎖雷厂。當(dāng)barrier block被放進(jìn)一個私有的并行隊列后,它不會被立刻執(zhí)行遍搞。實際為罗侯,隊列會等待直到當(dāng)前正在執(zhí)行的blocks執(zhí)行完畢器腋。到那個時刻溪猿,隊列才會自己執(zhí)行barrier block钩杰。而任何放到 barrier block之后的block直到 barrier block執(zhí)行完畢才會執(zhí)行。
     傳遞的隊列參數(shù)應(yīng)該是你自己用dispatch_queue_create函數(shù)創(chuàng)建的一個并行隊列诊县。如果你傳遞一個串行隊列或者全局并行隊列讲弄,這個函數(shù)的行為和 dispatch_sync相同。
     與dispatch_barrier_async不同依痊,它不會對目標(biāo)隊列進(jìn)行強(qiáng)引用(retain操作)避除。因為調(diào)用這個方法是同步的,它“借用”了調(diào)用者的引用胸嘁。而且瓶摆,沒有對block進(jìn)行Block_copy操作。
     作為對其優(yōu)化性宏,這個函數(shù)會在可能的情況下在當(dāng)前線程喚起barrier block群井。
     */
    
    // 為確保不會死鎖,當(dāng)前隊列是另一個隊列毫胜,而不能是self.barrierQueue书斜。
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        /*
        URLCallbacks字典類型key為NSURL類型,value為NSMutableArray類型酵使,value只包含著一個元素荐吉,這個元素是一個NSMutableDictionary類型,它的key為NSString代表著回調(diào)類型口渔,value為block样屠,是對應(yīng)的回調(diào)
        */
        // 同一時刻對相同url的多個下載請求只進(jìn)行一次下載
        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();
            /* 解釋
            若url第一次綁定它的回調(diào),也就是第一次使用這個url創(chuàng)建下載任務(wù)缺脉,則執(zhí)行一次創(chuàng)建回調(diào)瞧哟。
            在創(chuàng)建回調(diào)中創(chuàng)建下載操作,dispatch_barrier_sync執(zhí)行確保同一時間只有一個線程操作URLCallbacks屬性枪向,也就是確保了下面創(chuàng)建過程中在給operation傳遞回調(diào)的時候能取到正確的self.URLCallbacks[url]值勤揩。同時保證后面有相同的url再次創(chuàng)建時,if (!self.URLCallbacks[url])分支不再進(jìn)入秘蛔,first==NO,也就不再繼續(xù)調(diào)用創(chuàng)建回調(diào)陨亡。這樣就確保了同一個url對應(yīng)的圖片不會被重復(fù)下載。

            而下載器的完成回調(diào)中會將url從self.URLCallbacks中remove深员,雖然remove掉了负蠕,但是再次使用這個url進(jìn)行下載圖片的時候,Manager會向緩存中讀取下載成功的圖片了,而不是無腦地直接添加下載任務(wù)倦畅;即使之前的下載是失敗的(也就是說沒有緩存)遮糖,這樣繼續(xù)添加下載任務(wù)也是合情合理的。
            // 因此準(zhǔn)確地說叠赐,將這個block放到并行隊列dispatch_barrier_sync執(zhí)行確保了欲账,同一個url的圖片不會同一時刻進(jìn)行多次下載.
            
            // 這樣做還使得下載操作的創(chuàng)建同步進(jìn)行屡江,因為一個新的下載操作還沒有創(chuàng)建完成,self.barrierQueue會繼續(xù)等待它完成赛不,然后才能執(zhí)行下一個添加下載任務(wù)的block惩嘉。所以說SD添加下載任務(wù)是同步的,而且都是在self.barrierQueue這個并行隊列中踢故,同步添加任務(wù)文黎。這樣也保證了根據(jù)executionOrder設(shè)置依賴關(guān)是正確的。換句話說如果創(chuàng)建下載任務(wù)不是使用dispatch_barrier_sync完成的殿较,而是使用異步方法 耸峭,雖然依次添加創(chuàng)建下載操作A、B淋纲、C的任務(wù)抓艳,但實際創(chuàng)建順序可能為A、C帚戳、B玷或,這樣當(dāng)executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設(shè)置的操作依賴關(guān)系就變成了A依賴C片任,C依賴B
            // 但是添加之后的下載依然是在下載隊列downloadQueue中異步執(zhí)行偏友,絲毫不會影響到下載效率。

            // 以上就是說了SD下載的關(guān)鍵點:創(chuàng)建下載任務(wù)在barrierQueue隊列中对供,執(zhí)行下載在downloadQueue隊列中位他。
            */
        }
    });
}

關(guān)于SDWebImage的源碼閱讀就到這里結(jié)束,有什么不對的地方产场,歡迎指正鹅髓。

相關(guān)資料:
通讀SDWebImage①--總體梳理、下載和緩存
SDWebImage 源碼閱讀筆記(三)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末京景,一起剝皮案震驚了整個濱河市窿冯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌确徙,老刑警劉巖醒串,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鄙皇,居然都是意外死亡芜赌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門伴逸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缠沈,“玉大人,你說我怎么就攤上這事≈薹撸” “怎么了颓芭?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長禽篱。 經(jīng)常有香客問我,道長馍惹,這世上最難降的妖魔是什么躺率? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮万矾,結(jié)果婚禮上悼吱,老公的妹妹穿的比我還像新娘。我一直安慰自己良狈,他們只是感情好后添,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著薪丁,像睡著了一般遇西。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上严嗜,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天粱檀,我揣著相機(jī)與錄音,去河邊找鬼漫玄。 笑死茄蚯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的睦优。 我是一名探鬼主播渗常,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汗盘!你這毒婦竟也來了皱碘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤隐孽,失蹤者是張志新(化名)和其女友劉穎尸执,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缓醋,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡如失,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了送粱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褪贵。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脆丁,到底是詐尸還是另有隱情世舰,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布槽卫,位于F島的核電站跟压,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏歼培。R本人自食惡果不足惜震蒋,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躲庄。 院中可真熱鬧查剖,春花似錦、人聲如沸噪窘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倔监。三九已至直砂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浩习,已是汗流浹背哆键。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留瘦锹,地道東北人籍嘹。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像弯院,于是被迫代替她去往敵國和親辱士。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容