SDWebImage源碼分析 3

上章節中遺漏了storeImage這個方法,這章節補完,先來SDWebImageCache.h看它的注釋:

/**
 * 根據給的key將圖片存儲到內存以及磁盤(可選)緩存中
 *
 * @param image       需要存儲的圖片
 * @param recalculate 圖片數據能否在UIImage中被構建出來
 * @param imageData   從服務器返回的圖片數據
 * @param key         唯一的圖片緩存key, 常使用圖片的絕對URL
 * @param toDisk      設為YES時將圖片存儲進磁盤中
 */
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;

通過注釋得知該方法是將圖片存儲進緩存中用的,看下里面的代碼:

if (!image || !key) {
    return;
}
// 如果開啟內存緩存
if (self.shouldCacheImagesInMemory) { //< 默認是開啟的
    NSUInteger cost = SDCacheCostForImage(image); //< 計算cost
    [self.memCache setObject:image forKey:key cost:cost]; //< 利用內建的NSCache進行存儲
}
//如果開啟寫入磁盤
if (toDisk) {
    dispatch_async(self.ioQueue, ^{ //< 存入磁盤也是在異步中進行的
        NSData *data = imageData;

        if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
            // 這里主要的功能就是探測圖片是PNG還是JPEG
            // We need to determine if the image is a PNG or a JPEG
            // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
            // The first eight bytes of a PNG file always contain the following (decimal) values:
            // 137 80 78 71 13 10 26 10

            // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
            // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
            int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
            BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                              alphaInfo == kCGImageAlphaNoneSkipFirst ||
                              alphaInfo == kCGImageAlphaNoneSkipLast);
            BOOL imageIsPng = hasAlpha;

            // 如果有圖片數據,查看前8個字節,判斷是不是png
            if ([imageData length] >= [kPNGSignatureData length]) {
                imageIsPng = ImageDataHasPNGPreffix(imageData);
            }

            if (imageIsPng) {
                data = UIImagePNGRepresentation(image);
            }
            else {
                data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
            }
#else
            data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
        }

        [self storeImageDataToDisk:data forKey:key];
    });
}

通過代碼可以知道,圖片默認是通過url作為key被存儲進NSCache中的。

if (!imageData) {
    return;
}
//如果沒有對應目錄就生成一個
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}

// 為圖片key獲取緩存路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// 轉換成NSURL
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

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

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

如果需要將圖片緩存進磁盤中,就開一個子線程,在里面探測圖片是屬于哪種類型的,再通過storeImageDataToDisk方法存入磁盤,默認情況下不允許iCloud備份,又學到一招。

defaultCachePathForKey這個方法是用來生成緩存路徑的,跟蹤進去發現調用的是下面這個方法:

- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

這段代碼將key丟入cachedFileNameForKey方法中生成文件名,然后再將其追加到path后返回緩存路徑的。

我們再跟到cachedFileNameForKey中一探究竟:

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

該方法將url轉為了MD5命名,目的應該是防止緩存進磁盤的文件命名沖突

回到SDWebImageDownloader.m中,上期還沒講的downloadImageWithURL 方法:

- (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:^{
    ...

這里第一行就調用了addProgressCallback這個方法,跟蹤進去:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    //URL用作回調字典的鍵所以不允許為nil,如果為nil立即調用completed block傳入空的image,data,error
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new]; //< URLCallbacks中沒有對應的url就創建一個NSMutableArray
            first = YES;
        }

        //同一個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();
        }
    });
}

這段代碼中用到了dispatch_barrier_sync,當寫入線程與其他寫入線程或讀取線程并行時候會產生問題,比如:

//摘自《Objective-C高級編程 iOS與OS X多線程和內存管理》
dispatch_queue_t queue = dispatch_create("barrier",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue,blk0_for_reading);
dispatch_async(queue,blk1_for_reading);
dispatch_async(queue,blk2_for_reading);
dispatch_async(queue,blk_for_writing);//可能造成讀取的數據與預期不符,如果追加多個寫入則可能發生更多問題,比如數據競爭。
dispatch_async(queue,blk3_for_reading);
dispatch_async(queue,blk4_for_reading);
dispatch_async(queue,blk5_for_reading);

將寫入任務換成dispatch_barrier_async可以解決這個問題,它會等待這個blk02并發執行完畢后,開始執行。執行完畢之后才輪到blk35并發執行

另外,關注一下self.URLCallbacks[url]里存放的數據格。以url作為key,將completed和progress存放進去,如下所示:

{
 "http://www.xxx.com/xxx.jpg" = (
     {
         completed = "<__NSMallocBlock__: 0x7faa6b426620>";
         progress = "<__NSGlobalBlock__: 0x10a508b90>";
     }
 );
}

接著看addProgressCallback方法回調部分:

NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
    timeoutInterval = 15.0; //< 下載超時的時間,默認是15秒。
}

在沒設置downloadTimeout的情況下,默認超時時間為15秒

//開啟SDWebImageDownloaderUseNSURLCache選項則使用NSURL的Cache,否則忽略
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); //< 簡單地說就是請求時將cookie一起帶上
request.HTTPShouldUsePipelining = YES; //< 不等待上次請求響應,直接發送數據
if (wself.headersFilter) { //< 自定義http 頭部
    request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else { //< 默認http 頭部
    request.allHTTPHeaderFields = wself.HTTPHeaders;
}

這里用于構建NSMutableURLRequest以及設置http頭部信息

//wself.operationClass是Class類型,由init的時候或set時指定
//_operationClass = [SDWebImageDownloaderOperation class];
operation = [[wself.operationClass alloc] initWithRequest:request
  options:options
  progress:^(NSInteger receivedSize, NSInteger expectedSize) {
    ...
  }
  completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
    ...
  }
  cancelled:^{
    ...
  }];

這里將上面構造好的request放入SDWebImageDownloaderOperation中被初始化。三段回調里的代碼比較長,這里拆開來說

progress:

progress:^(NSInteger receivedSize, NSInteger expectedSize) {
 SDWebImageDownloader *sself = wself;
 if (!sself) return;
 __block NSArray *callbacksForURL;
 //此處并未使用dispatch_barrier_sync,個人感覺是因為此處并未對sself.URLCallbacks進行修改,所以不會對后面的任務產生數據競爭(競態條件),使用dispatch_sync更恰當
 dispatch_sync(sself.barrierQueue, ^{
     callbacksForURL = [sself.URLCallbacks[url] copy];
 });
 for (NSDictionary *callbacks in callbacksForURL) {
     dispatch_async(dispatch_get_main_queue(), ^{ //< 回主線程
         SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; //< process
         // receivedSize:下載了多少,expectedSize:預期下載量多大
         // 只要有新數據塊到達,這個block就會不停地被調用
         if (callback) callback(receivedSize, expectedSize);
     });
 }
}

completed:

completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
  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]; //< 任務完成后刪除對應url的回調
      }
  });
  for (NSDictionary *callbacks in callbacksForURL) {
      SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; //< completed
      if (callback) callback(image, data, error, finished);
  }
}

cancelled:

cancelled:^{
    SDWebImageDownloader *sself = wself;
    if (!sself) return;
    dispatch_barrier_async(sself.barrierQueue, ^{
        [sself.URLCallbacks removeObjectForKey:url];//< 任務取消后刪除對應url的回調
    });
}
//圖片在下載后以及在緩存中進行解壓可以提高性能但會消耗大量內存,默認YES
operation.shouldDecompressImages = wself.shouldDecompressImages;
//鑒權相關
if (wself.urlCredential) {
    operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
    operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
//優先級相關
if (options & SDWebImageDownloaderHighPriority) {
    operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
    operation.queuePriority = NSOperationQueuePriorityLow;
}
//將operation加入到downloadQueue
[wself.downloadQueue addOperation:operation];

if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { //< 如果執行順序為LIFO后進先出
    // 將新添加的operations作為最后一個operation的依賴,執行順序就是新添加的operation先執行,后進先出的過程,類似棧結構
    // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

至此本章將SDWebImageDownloader的主要流程給介紹完了。對于網絡請求鑒權什么的我不打算深究,留著以后看完AFNetWorking再說。

P.S:順便吐槽下簡書的代碼顯示太窄,影響閱讀效果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容