iOS源碼補(bǔ)完計(jì)劃--SDWebImage4.0+源碼參閱(附面試題/流程圖)

年也過完了媚狰、決定補(bǔ)完一下入行時(shí)就欠下的債。

參拜一下SDWebImage的源碼诱篷。

并不是說一定要讀如何如何壶唤、只是覺得源碼的閱讀是一種很好的學(xué)習(xí)方式。無論從架構(gòu)還是技術(shù)點(diǎn)方面棕所。


目錄

  • 常見疑問(面試大全视粮?)
    • 磁盤目錄位于哪里?
    • 最大并發(fā)數(shù)橙凳、超時(shí)時(shí)長蕾殴?
    • 圖片如何命名?
    • 如何識(shí)別圖片類型?
    • 所查找到的圖片的來源?
    • 所有下載的圖片都將被寫入緩存岛啸?磁盤呢钓觉?何時(shí)緩存的?
    • 磁盤緩存的時(shí)長坚踩?清理操作的時(shí)間點(diǎn)荡灾?
    • 磁盤清理的原則?
    • 下載圖片時(shí)、會(huì)使用緩存協(xié)議么?
    • 下載圖片的URL必須是NSURL么批幌?
    • 讀取緩存以及讀取磁盤的時(shí)候如何保證線程安全础锐?
  • 相關(guān)知識(shí)點(diǎn)
    • NS_OPTIONS枚舉與位運(yùn)算
    • 內(nèi)聯(lián)函數(shù)
  • 準(zhǔn)備工作
  • 工作原理
  • 業(yè)務(wù)層級(jí)
  • 核心代碼(正常讀取下載圖片)
    • 最上層:UIView+WebCache
    • 邏輯層:SDWebImageManager
    • 業(yè)務(wù)層:
      • 緩存&&磁盤操作(SDImageCache)
      • 下載操作(SDWebImageDownloader)
  • 一些啟發(fā)
    • 分層的接口API設(shè)計(jì)
    • 線程安全
    • 內(nèi)聯(lián)函數(shù)
    • 精細(xì)的緩存管理原則
    • 回調(diào)設(shè)計(jì)

常見疑問(面試大全?)

雖然我更推薦閱讀源碼荧缘、可如果實(shí)在沒時(shí)間皆警。這一段只要花費(fèi)幾分鐘。
我還是比較喜歡把干貨放在前面截粗、方便伸手黨(比如我)信姓。
不過也不能保證涵蓋全部問題、歡迎留言绸罗。

  • 磁盤目錄位于哪里意推?

緩存在磁盤沙盒目錄下 Library/Caches
二級(jí)目錄為~/Library/Caches/default/com.hackemist.SDWebImageCache.default

- (instancetype)init {
    return [self initWithNamespace:@"default"];
    //   ~Library/Caches/default
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]

        // Init the disk cache
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }

//  _diskCachePath = ~/Library/Caches/default/com.hackemist.SDWebImageCache.default
}

你也可以通過[[SDImageCache sharedImageCache] addReadOnlyCachePath:bundledPath];來自定義一個(gè)路徑。

但這個(gè)路徑不會(huì)被存儲(chǔ)使用珊蟀、是給開發(fā)者自定義預(yù)裝圖片的路徑菊值。
  • 最大并發(fā)數(shù)、超時(shí)時(shí)長育灸?

_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadTimeout = 15.0;
  • 圖片如何命名腻窒?

這里寫入緩存和寫入磁盤是不同的。
寫入緩存時(shí)描扯、直接用圖片url作為key

//寫入緩存
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];

寫入磁盤時(shí)、用url的MD5編碼作為key趟薄≌莱希可以防止文件名過長

- (nullable NSString *)cachedFileNameForKey:(nullable 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);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    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], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
    return filename;
    //key == https://gss2.bdstatic.com/-fo3dSag_xI4khGkpoWK1HF6hhy/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=034361ab922397ddc274905638ebd9d2/d31b0ef41bd5ad64dddebb.jpg;
    //filename == f029945f95894e152771806785bc4f18.jpg;
}
  • 如何識(shí)別圖片類型?

通過NSData數(shù)據(jù)的第一個(gè)字符進(jìn)行判斷。

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52: {
            if (data.length >= 12) {
                //RIFF....WEBP
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                    return SDImageFormatWebP;
                }
            }
            break;
        }
        case 0x00: {
            if (data.length >= 12) {
                //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) {
                    return SDImageFormatHEIC;
                }
            }
            break;
        }
    }
    return SDImageFormatUndefined;
}
  • 所查找到的圖片的來源?

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * 從網(wǎng)上下載
    */
    SDImageCacheTypeNone,
    /**
     * 從磁盤獲得
     */
    SDImageCacheTypeDisk,
    /**
     * 從內(nèi)存獲得
     */
    SDImageCacheTypeMemory
};
  • 所有下載的圖片都將被寫入緩存杭煎?磁盤呢恩够?何時(shí)緩存的?

磁盤不是強(qiáng)制寫入羡铲。從枚舉SDWebImageOptions可見

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
   
    /**
     *  禁用磁盤緩存
     */
    SDWebImageCacheMemoryOnly = 1 << 2,
}

而Memory緩存應(yīng)該是必須寫入的(因?yàn)槲也]找到哪里可以禁止)蜂桶。
緩存的時(shí)間點(diǎn)、有兩個(gè)(開發(fā)者也可以主動(dòng)緩存)也切、且都是由SDWebImageManager進(jìn)行扑媚。
其一是下載成功后、自動(dòng)保存雷恃〗桑或者開發(fā)者通過代理處理圖片并返回后緩存

- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;


=========>>SDWebImageManager
//獲取轉(zhuǎn)換用戶后的圖片
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

//用戶處理成功
if (transformedImage && finished) {
      BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
      
      //用戶處理的后若未生成新的圖片、則保存下載的二進(jìn)制文件倒槐。
      //不然則由imageCache內(nèi)部生成二進(jìn)制文件保存
      [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}

其二是當(dāng)緩存中沒有旬痹、但是從硬盤中查詢到了圖片。

@autoreleasepool {
        //搜索硬盤
        NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        UIImage *diskImage = [self diskImageForKey:key];
        //緩存到內(nèi)存、默認(rèn)為YES
        if (diskImage && self.config.shouldCacheImagesInMemory) {
             NSUInteger cost = SDCacheCostForImage(diskImage);
             //使用NSChache緩存两残。
             [self.memCache setObject:diskImage forKey:key cost:cost];
        }
       if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
               doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
             });
        }
}
  • 磁盤緩存的時(shí)長永毅?清理操作的時(shí)間點(diǎn)?

默認(rèn)為一周
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

能夠以時(shí)間清除磁盤的方法為

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

調(diào)用的時(shí)機(jī)為

[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
 [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];

也就是當(dāng)程序退出到后臺(tái)人弓、或者被殺死的時(shí)候沼死。
這里、還有另外一個(gè)點(diǎn)票从。
Long-Running Task任務(wù)

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    //后臺(tái)任務(wù)標(biāo)識(shí)--注冊(cè)一個(gè)后臺(tái)任務(wù)
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        //超時(shí)(大概150秒漫雕?)自動(dòng)結(jié)束后臺(tái)任務(wù)
        //結(jié)束后臺(tái)任務(wù)
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    
    [self deleteOldFilesWithCompletionBlock:^{
        
        //結(jié)束后臺(tái)任務(wù)
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

正常程序在進(jìn)入后臺(tái)后、雖然可以繼續(xù)執(zhí)行任務(wù)峰鄙。但是在時(shí)間很短內(nèi)就會(huì)被掛起待機(jī)浸间。
Long-Running可以讓系統(tǒng)為app再多分配一些時(shí)間來處理一些耗時(shí)任務(wù)。

  • 磁盤清理的原則吟榴?

首先魁蒜、通過時(shí)間進(jìn)行清理。(最后修改時(shí)間>一周)
然后吩翻、根據(jù)占據(jù)內(nèi)存大小進(jìn)行清理兜看。(如果占據(jù)內(nèi)存大于上限、則按時(shí)間排序狭瞎、刪除到上限的1/2细移。)
這里我并沒有看到使用頻率優(yōu)先級(jí)判斷、所以應(yīng)該是沒有熊锭。

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    //異步清理超時(shí)圖片
    dispatch_async(self.ioQueue, ^{
        //獲取磁盤目錄
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        //NSURLIsDirectoryKey 判斷是否為目錄
        //NSURLContentModificationDateKey 判斷最后修改時(shí)間
        //NSURLTotalFileAllocatedSizeKey 判斷文件大小
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        //模具器--遍歷磁盤路徑下的文件
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        //計(jì)算一周前(需要釋放)弧轧、的時(shí)間
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        //保存緩存文件Dic
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        //緩存總大小
        NSUInteger currentCacheSize = 0;
        //需要?jiǎng)h除的url路徑
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        //遍歷磁盤文件枚舉器
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            //獲取每個(gè)文件所對(duì)應(yīng)的三個(gè)參數(shù)(resourceKeys)
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // Skip directories and errors.
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                //如果是文件夾則跳過
                continue;
            }

            // Remove files that are older than the expiration date;
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                //如果時(shí)間超過指定日期、加入刪除數(shù)組碗殷。跳過
                [urlsToDelete addObject:fileURL];
                continue;
            }
            //獲取文件大小精绎、并且把路徑與大小存入字典。
            // Store a reference to this file and account for its total size.
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        
        //遍歷刪除文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        //如果剩余文件大小仍超過閾值
        //優(yōu)先刪除最老的文件
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // 將剩余的文件按修改時(shí)間排序
            NSArray<NSURL *> *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<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                    //直到低于閾值的二分之一
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        //回調(diào)給主線程
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}
  • 下載圖片時(shí)锌妻、會(huì)使用網(wǎng)絡(luò)協(xié)議緩存邏輯么?

默認(rèn)情況下不會(huì)代乃、由以下代碼可見。

NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];

除非將options配置成SDWebImageDownloaderUseNSURLCache仿粹、否則每次都會(huì)從原地址重新下載搁吓、而不是用網(wǎng)絡(luò)協(xié)議的緩存邏輯。

  • 下載圖片的URL必須是NSURL么吭历?

不是擎浴、在SDWebImageManager中有過容錯(cuò)處理。所以即便你傳入一個(gè)字符串毒涧、依舊可以正確的查找贮预。

if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

但是由于API暴露出的是(nullable NSURL *)贝室、如果你傳入字符串、會(huì)有黃色警告


  • 讀取緩存以及讀取磁盤的時(shí)候如何保證線程安全仿吞?

  • 讀取緩存
    讀取緩存的時(shí)候是在主線程進(jìn)行滑频。由于使用NSCache進(jìn)行存儲(chǔ)、所以不需要擔(dān)心單個(gè)value對(duì)象的線程安全唤冈。
  • 讀取磁盤
    磁盤的讀取雖然創(chuàng)建了一個(gè)NSOperation對(duì)象峡迷、但據(jù)我所見這個(gè)對(duì)象只是用來標(biāo)記該操作是否被取消、以及取消之后不再讀取磁盤文件的作用你虹。
    真正的磁盤緩存是在另一個(gè)IO專屬線程中的一個(gè)串行隊(duì)列下進(jìn)行的绘搞。
    如果你搜索self.ioQueue還能發(fā)現(xiàn)、不只是讀取磁盤內(nèi)容傅物。
    包括刪除夯辖、寫入等所有磁盤內(nèi)容都是在這個(gè)IO線程進(jìn)行、以保證線程安全董饰。
    但計(jì)算大小蒿褂、獲取文件總數(shù)等操作。則是在主線程進(jìn)行卒暂。

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
==========>>>>><<<<<<===========
NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }

        @autoreleasepool {
            //搜索硬盤
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            //緩存到內(nèi)存啄栓、默認(rèn)為YES
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //使用NSChache緩存。
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    return operation;

相關(guān)知識(shí)點(diǎn)

如果對(duì)一些知識(shí)點(diǎn)不了解也祠、可能對(duì)代碼理解造成困擾昙楚。列舉一下。

  • NS_OPTIONS枚舉與位運(yùn)算

上文中的SDWebImageOptions便是一個(gè)位移枚舉

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,
    SDWebImageLowPriority = 1 << 1,
    SDWebImageCacheMemoryOnly = 1 << 2,        
    SDWebImageProgressiveDownload = 1 << 3,
    SDWebImageRefreshCached = 1 << 4,
    SDWebImageContinueInBackground = 1 << 5,
    SDWebImageHandleCookies = 1 << 6,
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    SDWebImageHighPriority = 1 << 8,
    SDWebImageDelayPlaceholder = 1 << 9,
    SDWebImageTransformAnimatedImage = 1 << 10,
    SDWebImageAvoidAutoSetImage = 1 << 11,
    SDWebImageScaleDownLargeImages = 1 << 12
};

和我們普通用的枚舉

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    SDImageCacheTypeNone,
    SDImageCacheTypeDisk,
    SDImageCacheTypeMemory
};

從表面看有兩點(diǎn)不同:

  • 枚舉聲明:NS_ENUM&& NS_OPTIONS
    其實(shí)從定義的效果上來講诈嘿、二者作用相同堪旧。
    更多的是語義化的角度。前者是普通枚舉永淌、后者是位移枚舉崎场。
  • 枚舉中的位運(yùn)算符號(hào)<<.
    位運(yùn)算中佩耳、有三種基本運(yùn)算符號(hào).
  • 按位與"&"

只有對(duì)應(yīng)的兩個(gè)二進(jìn)位均為1時(shí)遂蛀,結(jié)果位才為1,否則為0
比如9&5干厚,其實(shí)就是1001&0101=0001李滴,因此9&5=1>二進(jìn)制中,與1相&就保持原位蛮瞄,與0相&就為0

  • 按位或"|"

只要對(duì)應(yīng)的二個(gè)二進(jìn)位有一個(gè)為1時(shí)所坯,結(jié)果位就為1,否則為0挂捅。
比如9|5芹助,其實(shí)就是1001|0101=1101,因此9|5=13

  • 左移"<<"

把整數(shù)a的各二進(jìn)位全部左移n位,高位丟棄状土,低位補(bǔ)0无蜂。左移n位其實(shí)就是乘以2的n次方。
例如1<<2 就是0001左移2為0100蒙谓,因此1<<2=4

于是斥季、在使用位移枚舉的時(shí)候、我們就有了這種寫法:
options:SDWebImageRetryFailed | SDWebImageCacheMemoryOnly];

上面的意思是累驮。這個(gè)操作是如果失敗了需要重試酣倾、并且只寫入緩存。
其中 options=SDWebImageRetryFailed | SDWebImageCacheMemoryOnly
也就是0b00000001| 0b00000100 = 0b00000101 十進(jìn)制中 = 5.

在內(nèi)部判斷時(shí)候就有了如下寫法:
//是否磁盤緩存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

等價(jià)于 0101 & 0100 = 0100 結(jié)果為真谤专。
倘若

BOOL lowPriority = !(options & SDWebImageLowPriority);

等價(jià)于 0101 & 0010 = 0000 結(jié)果為假躁锡。

  • 內(nèi)聯(lián)函數(shù)

在寫入緩存時(shí)、出現(xiàn)了這樣一行代碼

NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];

其中SDCacheCostForImage指向一個(gè)靜態(tài)內(nèi)聯(lián)函數(shù)

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
    return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
    return image.size.height * image.size.width * image.scale * image.scale;
#endif
}

其中FOUNDATION_STATIC_INLINE作為宏指向static inline毒租、所以也等價(jià)于

static __inline__ NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
    return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
    return image.size.height * image.size.width * image.scale * image.scale;
#endif
}

用宏寫方法稚铣、我們都用過。但是表達(dá)式形式的宏定義有一定的弊端墅垮。(比如參數(shù)檢查惕医、越界等等)。

內(nèi)聯(lián)函數(shù)完全可以取代表達(dá)式形式的宏定義算色。

順便談?wù)劄槭裁匆脙?nèi)聯(lián)函數(shù)吧抬伺。

  • 效率來看
    • 函數(shù)之間調(diào)用,是內(nèi)存地址之間的調(diào)用灾梦、當(dāng)函數(shù)調(diào)用完畢之后還會(huì)返回原來函數(shù)執(zhí)行的地址虐先。函數(shù)調(diào)用將會(huì)有時(shí)間開銷。
    • 內(nèi)聯(lián)函數(shù)在匯編中沒有call語句肉康。取消了函數(shù)的參數(shù)壓棧
  • 相比表達(dá)式形式的宏定義
    • 需要預(yù)編譯.因?yàn)閕nline內(nèi)聯(lián)函數(shù)也是函數(shù)募强、不需要預(yù)編譯。
    • 調(diào)用時(shí)候會(huì)首先檢查它的參數(shù)的類型萧福、保證調(diào)用正確拉鹃。
    • 可以使用所在類的保護(hù)成員及私有成員。
需要注意的是
  • 內(nèi)聯(lián)函數(shù)中盡量不要使用諸如循環(huán)語句等大量代碼鲫忍、可能會(huì)導(dǎo)致編譯器放棄內(nèi)聯(lián)動(dòng)作膏燕。
  • 內(nèi)聯(lián)函數(shù)的定義須在調(diào)用之前。

準(zhǔn)備工作

隨手下載了一個(gè)最新的 (4.2.3)

GitHub

PODS:
- SDWebImage (4.2.3):
- SDWebImage/Core (= 4.2.3)
- SDWebImage/Core (4.2.3)

DEPENDENCIES:
- SDWebImage

SPEC CHECKSUMS:
SDWebImage: 791bb72962b3492327ddcac4b1880bd1b5458431

PODFILE CHECKSUM: 7fbc0b76fb4d0b0b2afa7d3a90b7bd68dea25abb

COCOAPODS: 1.3.1


工作原理

引用GitHub上一個(gè)導(dǎo)圖


  • 1悟民、外部API入口坝辫。
    通過UIImageView+WebCachesd_setImageWithURL方法(等)作為入口來加載圖片。
  • 2射亏、內(nèi)部API匯總近忙。
    通過UIView+WebCache的'sd_internalSetImageWithURL'對(duì)UIImageView竭业、UIButton 、MKAnnotationView中圖片的下載請(qǐng)求進(jìn)行匯總及舍。
  • 3永品、開始加載圖片。
    通過SDWebImageManagerloadImageWithURL對(duì)圖片進(jìn)行加載击纬。
  • 4鼎姐、查找本地
    通過SDImageCachequeryCacheOperationForKey查找緩存中是否存在圖片。如果不存在再通過diskImageDataBySearchingAllPathsForKey進(jìn)行磁盤搜索更振。
  • 5炕桨、返回本地圖片給SDWebImageManager
  • 6、下載圖片
    如果本地查詢不到對(duì)應(yīng)圖片肯腕、則通過SDImageDownloaderdownloadImage進(jìn)行圖片下載献宫。
  • 7、下載完畢返回圖片給SDWebImageManager
  • 8实撒、由UIView+WebCache通過storeImage將下載圖片保存本地
  • 9姊途、返回圖片給UIView+WebCache
  • 10、設(shè)置圖片
    其中知态。

業(yè)務(wù)層級(jí)

  • 整個(gè)架構(gòu)簡單分為三層捷兰。

最上層:

負(fù)責(zé)業(yè)務(wù)的接入、圖片的插入

#import "UIImageView+WebCache.h"
#import "UIButton+WebCache.h"
#import "UIImageView+HighlightedWebCache.h"
//以及其匯總的
#import "UIView+WebCache.h"

邏輯層

負(fù)責(zé)不同類型業(yè)務(wù)的分發(fā)负敏。
讀取(或?qū)懭?緩存(或磁盤)贡茅、下載等具體邏輯處理。

#import "SDWebImageManager.h"

業(yè)務(wù)層

負(fù)責(zé)具體業(yè)務(wù)的實(shí)現(xiàn)

//緩存&&磁盤操作
#import "SDImageCache.h"
//下載操作
#import "SDWebImageDownloader.h"

當(dāng)然其做、還有其他的工具類顶考。但主要的、就是上面幾個(gè)妖泄。


核心代碼(正常讀取下載圖片)

  • 最上層:UIView+WebCache

所有的代碼最終都會(huì)匯總到

#import "UIView+WebCache.h"
/**
 * @param url            圖片地址鏈接
 * @param placeholder    占位圖
 * @param options        下載圖片的枚舉驹沿。包括優(yōu)先級(jí)、是否寫入硬盤等
 * @param operationKey   一個(gè)記錄當(dāng)前對(duì)象正在加載操作的key蹈胡、保證只有最新的操作在進(jìn)行渊季、默認(rèn)為類名。
                         所以如果你想下載多個(gè)圖片并且都展示一下审残、可以嘗試自定義幾個(gè)operationKey來操作梭域。(我猜)
 * @param setImageBlock  給開發(fā)者自定義set圖片的callback
 * @param progressBlock  下載進(jìn)度callback
 * @param completedBlock 下載完成的callback(sd已經(jīng)給你set好了斑举、只是會(huì)把圖片給你罷了)
 * @param context        一些額外的上下文字典搅轿。比如你可以搞一個(gè)專屬的imageManager進(jìn)來干活。
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary *)context {
    //以當(dāng)前實(shí)例的class作為OperationKey
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    //清除當(dāng)前OperationKey下正在進(jìn)行的操作富玷。節(jié)省無用功
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    //給對(duì)象實(shí)例綁定imageURLKey = url
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //是否先加載占位圖
    if (!(options & SDWebImageDelayPlaceholder)) {
        if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
            dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
            dispatch_group_enter(group);
        }
        //到主線城更新UI
        dispatch_main_async_safe(^{
            //set 占位圖
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // 小菊花
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        // 允許開發(fā)者指定一個(gè)manager來進(jìn)行操作
        SDWebImageManager *manager;
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }
        
        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            //圖片下載||讀取完成
            __strong __typeof (wself) sself = wself;
            //小菊花
            [sself sd_removeActivityIndicator];
            if (!sself) { return; }
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            //是否不插入圖片
            //1璧坟、有圖片既穆、但是主動(dòng)配置
            //2、沒圖片雀鹃、設(shè)置了延遲加載占位圖
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                //
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    //操作完成的回調(diào)
                    completedBlock(image, error, cacheType, url);
                }
            };
            
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            if (shouldNotSetImage) {
                //如果不顯示圖片幻工、直接回調(diào)。
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            /**自動(dòng)插入圖片***/

            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }
            
            if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
                dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey];
                dispatch_group_enter(group);
                dispatch_main_async_safe(^{
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                });
                // ensure completion block is called after custom setImage process finish
                dispatch_group_notify(group, dispatch_get_main_queue(), ^{
                    callCompletedBlockClojure();
                });
            } else {
                dispatch_main_async_safe(^{
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    callCompletedBlockClojure();
                });
            }
        }];
        
        //在讀取圖片之前黎茎。向正在進(jìn)行加載的HashMap中加入當(dāng)前operation
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

一個(gè)簡單的流程圖


UIView+WebCache流程圖
  • 邏輯層:SDWebImageManager

SDWebImage中最核心的類囊颅、調(diào)度這圖片的下載(SDWebImageDownloader)以及緩存(SDImageCache)。

此外傅瞻、SDWebImageManager并不依托于UIView+WebCache踢代、完全可以單獨(dú)使用。
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {

    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    //所以嗅骄、我們并不需要在外部把字符串變?yōu)镹SURL胳挎。
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    //下載操作的對(duì)象
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    if (url) {
        @synchronized (self.failedURLs) {
            //線程安全
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }

    //url為空 || (未設(shè)置失敗重試 && 這個(gè)url已經(jīng)失敗過)
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        //發(fā)出一個(gè)獲取失敗的回調(diào)
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

    //將操作添加到正在進(jìn)行的操作數(shù)池
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    //默認(rèn)就是url作為key、也可以自定義mananger的相關(guān)block
    NSString *key = [self cacheKeyForURL:url];
    //通過key溺森、查找本地圖片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            //操作被取消慕爬、移除操作池
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }

        //本地沒有圖片 || 刷新緩存
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            //有本地圖片。但需要被刷新
            if (cachedImage && options & SDWebImageRefreshCached) {
                //先回調(diào)出去本地圖片屏积。再繼續(xù)下載操作
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            //下面是根據(jù)調(diào)用者傳進(jìn)來的option医窿,來匹配設(shè)置了哪些,就給downloaderOptions賦值哪些option
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            if (cachedImage && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            //下載圖片
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                } else if (error) {
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];

                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost
                        && error.code != NSURLErrorNetworkConnectionLost) {
                        @synchronized (self.failedURLs) {
                            //失敗記錄
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                        //失敗重新下載
                        @synchronized (self.failedURLs) {
                            //從失敗記錄移除
                            [self.failedURLs removeObject:url];
                        }
                    }
                    //是否磁盤緩存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        //縮放
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
    
                        //是否需要轉(zhuǎn)換圖片
                        //成功下載圖片炊林、自定義實(shí)現(xiàn)了圖片處理的代理
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            //獲取轉(zhuǎn)換用戶后的圖片
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            //用戶處理成功
                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
      
                                //用戶處理的后若未生成新的圖片留搔、則保存下載的二進(jìn)制文件。
                                //不然則由imageCache內(nèi)部生成二進(jìn)制文件保存
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //回調(diào)
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        //下載成功且未自定義代理--默認(rèn)保存
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            @synchronized(operation) {
                operation.cancelBlock = ^{
                    [self.imageDownloader cancel:subOperationToken];
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    [self safelyRemoveOperationFromRunning:strongOperation];
                };
            }
        } else if (cachedImage) {
            //本地有圖片--回調(diào)铛铁、關(guān)閉當(dāng)前操作
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            //本地沒有隔显、也不下載--回調(diào)、關(guān)閉當(dāng)前操作
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;
}
SDWebImageManager流程圖
  • 業(yè)務(wù)層:

緩存&&磁盤操作(SDImageCache)

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    // First check the in-memory cache...
    //搜索磁盤緩存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        if (image.images) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        
        @autoreleasepool {
            //搜索硬盤
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            //緩存到內(nèi)存饵逐、默認(rèn)為YES
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //使用NSChache緩存括眠。
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    return operation;
}

//查詢緩存
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    //self.memCache  為NSCache實(shí)例
    return [self.memCache objectForKey:key];
}

//查詢磁盤
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    if (data) {
        //圖片解碼、調(diào)整方向
        UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
        //調(diào)整圖片縮放比例 @2x/@3x
        image = [self scaledImageForKey:key image:image];
        //壓縮圖片
        if (self.config.shouldDecompressImages) {
            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
        }
        return image;
    } else {
        return nil;
    }
}

//寫入緩存 && 磁盤
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    if (self.config.shouldCacheImagesInMemory) {
        //寫入緩存
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        //寫入磁盤
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                NSData *data = imageData;
                if (!data && image) {
                    // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
                    SDImageFormat format;
                    if (SDCGImageRefContainsAlpha(image.CGImage)) {
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                [self storeImageDataToDisk:data forKey:key];
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

//正式寫入磁盤
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
     
    [self checkIfQueueIsIOQueue];
    //如果文件中不存在磁盤緩存路徑 則創(chuàng)建
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
     
    // get cache Path for image key  得到該key的緩存路徑
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl  將緩存路徑轉(zhuǎn)化為url
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //將imageData存儲(chǔ)起來
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
     
    // disable iCloud backup  如果調(diào)用者關(guān)閉icloud 關(guān)閉iCloud備份
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

由于此處只歸納正常讀取下載流程的代碼倍权、所以其余關(guān)于圖片過期&&釋放流程的代碼沒有列出掷豺。后面會(huì)逐一進(jìn)行歸納。


查找本地流程圖

下載操作(SDWebImageDownloader)

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        //創(chuàng)建下載operation
        __strong __typeof (wself) sself = wself;
        //超時(shí)時(shí)間
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        
        //創(chuàng)建下載策略
        //SDWebImageDownloaderUseNSURLCache 則使用 NSURLRequestUseProtocolCachePolicy 緩存協(xié)議
        //默認(rèn)NSURLRequestReloadIgnoringLocalCacheData從原地址重新下載
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        
        //創(chuàng)建下載請(qǐng)求
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            //默認(rèn) image/*;q=0.8
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        
        //創(chuàng)建下載操作
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        //是否解壓
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        //證書
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            //默認(rèn) 賬號(hào)密碼為空的通用證書
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        //優(yōu)先級(jí)薄声。默認(rèn)都不是
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //向下載隊(duì)列 NSOperationQueue 中 添加本次下載操作
        [sself.downloadQueue addOperation:operation];
        
        //設(shè)置下載的順序 是按照隊(duì)列還是棧
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

//通過progressBlock&&completedBlock以及Url和SDWebImageDownloaderOperation對(duì)token進(jìn)行包裝
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            operation = createCallback();
            //將url作為key当船、對(duì)應(yīng)的下載操作operation作為value保存。
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                dispatch_barrier_sync(self.barrierQueue, ^{
                    SDWebImageDownloaderOperation *soperation = woperation;
                    if (!soperation) return;
                    if (self.URLOperations[url] == soperation) {
                        //下載完成默辨、移除操作
                        [self.URLOperations removeObjectForKey:url];
                    };
                });
            };
        }
        
        //將成progressBlock以及completedBlock組裝成SDCallbacksDictionary.
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        //生成下載任務(wù)標(biāo)識(shí)德频。用于manager將來定位對(duì)應(yīng)操作用
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

SDWebImageDownloaderOperation是具體下載操作、設(shè)計(jì)很多網(wǎng)絡(luò)層的東西缩幸。將來可以單獨(dú)開一篇壹置、結(jié)合AFNetWorking沒準(zhǔn)會(huì)更好竞思。

一些啟發(fā)

  • 分層的接口API設(shè)計(jì)。
#import "UIImageView+WebCache.h"
#import "UIButton+WebCache.h"
#import "UIImageView+HighlightedWebCache.h"
//以及其匯總的
#import "UIView+WebCache.h"

所有外層API與具體業(yè)務(wù)無關(guān)钞护。
使得SDWebImageManager可以脫離View層單獨(dú)運(yùn)作盖喷。

  • 線程安全
@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
if (url) {
     @synchronized (self.failedURLs) {
         isFailedUrl = [self.failedURLs containsObject:url];
     }
}
.....

所有可能引起資源搶奪的對(duì)象操作、全部有條件鎖保護(hù)难咕。
但是由于內(nèi)嵌異常處理代碼的存在课梳、條件鎖的性能是所有鎖中最差的。不知道為什么SD中使用這么多余佃。

  • 內(nèi)聯(lián)函數(shù)

更高效的短函數(shù)執(zhí)行惦界、替代表達(dá)式形式的宏定義。

  • 精細(xì)的緩存管理原則

詳參上文提到的《磁盤清理的原則咙冗?》

  • 回調(diào)設(shè)計(jì)

SDWebImage中使用了兩種沾歪、Block以及Delegate。

  • Block使用的很多雾消、舉兩個(gè)例子灾搏。
======>#import "UIView+WebCache.h"
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary *)context;

======>SDWebImageDownloader
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

再來看代理

@protocol SDWebImageManagerDelegate <NSObject>

@optional

/**
 * Controls which image should be downloaded when the image is not found in the cache.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param imageURL     The url of the image to be downloaded
 *
 * @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
 */
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

不難看出、SDWebImage對(duì)回調(diào)的使用傾向于:

  • Block
    單個(gè)圖片的分類立润、單個(gè)圖片的下載狂窑。
    每個(gè)操作任務(wù)中必現(xiàn)的progress以及completed。
    所以桑腮、有很強(qiáng)的個(gè)體綁定需要或者使用次數(shù)不多時(shí)泉哈、傾向使用block
  • Delegate
    SDWebImageManager下載完成之后的自定義圖片處理、是否下載某個(gè)url破讨。
    這兩個(gè)方法如果需要的話都是將會(huì)調(diào)用多次的丛晦。所以、用Delegate更好提陶、可以將方法常駐烫沙。
  • 同理
    UITableView的使用Delegate、是用為在滾動(dòng)途中隙笆、代理方法需要被不斷的執(zhí)行锌蓄。
    UIButton也是將會(huì)被多次點(diǎn)擊。
    UIView的動(dòng)畫/GCD則可以使用Block撑柔、因?yàn)橹粓?zhí)行一次瘸爽、用完釋放。
    所以铅忿、在日常使用中剪决、我們也可以參考上述原則進(jìn)行設(shè)計(jì)。
  • NSMapTable

用NSMapTable代替字典來存儲(chǔ)當(dāng)前正在進(jìn)行的操作、并且將value設(shè)置為NSMapTableWeakMemory昼捍。防止對(duì)應(yīng)value因?yàn)閺?qiáng)引用不能自動(dòng)釋放。


暫時(shí)想到的就這些肢扯、更多問題歡迎留言妒茬。


最后

本文主要是自己的學(xué)習(xí)與總結(jié)。如果文內(nèi)存在紕漏蔚晨、萬望留言斧正乍钻。如果不吝賜教小弟更加感謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末铭腕,一起剝皮案震驚了整個(gè)濱河市银择,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌累舷,老刑警劉巖浩考,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異被盈,居然都是意外死亡析孽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門只怎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來袜瞬,“玉大人,你說我怎么就攤上這事身堡〉擞龋” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵贴谎,是天一觀的道長汞扎。 經(jīng)常有香客問我,道長擅这,這世上最難降的妖魔是什么佩捞? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮蕾哟,結(jié)果婚禮上一忱,老公的妹妹穿的比我還像新娘。我一直安慰自己谭确,他們只是感情好帘营,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著逐哈,像睡著了一般芬迄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昂秃,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天禀梳,我揣著相機(jī)與錄音杜窄,去河邊找鬼。 笑死算途,一個(gè)胖子當(dāng)著我的面吹牛塞耕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘴瓤,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼扫外,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了廓脆?” 一聲冷哼從身側(cè)響起筛谚,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎停忿,沒想到半個(gè)月后驾讲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡席赂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年蝎毡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氧枣。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沐兵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出便监,到底是詐尸還是另有隱情扎谎,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布烧董,位于F島的核電站毁靶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏逊移。R本人自食惡果不足惜预吆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胳泉。 院中可真熱鬧拐叉,春花似錦、人聲如沸扇商。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽案铺。三九已至蔬芥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背笔诵。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工返吻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人乎婿。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓测僵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親次酌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子恨课,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359