[iOS 開發(fā)] SDWebImage 源碼閱讀筆記

前不久做了一個(gè)生成快照的需求卒密,其中用到 SDWebImage 來(lái)下載圖片揭绑,在使用該框架的過(guò)程中也遇到了一些問(wèn)題,索性正好就把 SDWebImage (v3.7.3) 源碼細(xì)讀了一下辞色,學(xué)習(xí)一下其中的設(shè)計(jì)思想和技術(shù)點(diǎn)锯厢,為了梳理思路书斜,順便寫下了這篇文章。

目錄

一、簡(jiǎn)介

1. 設(shè)計(jì)目的

SDWebImage 提供了 UIImageView造寝、UIButtonMKAnnotationView 的圖片下載分類吭练,只要一行代碼就可以實(shí)現(xiàn)圖片異步下載和緩存功能诫龙。這樣開發(fā)者就無(wú)須花太多精力在圖片下載細(xì)節(jié)上,專心處理業(yè)務(wù)邏輯鲫咽。

2. 特性

  • 提供 UIImageView, UIButton, MKAnnotationView 的分類签赃,用來(lái)顯示網(wǎng)絡(luò)圖片谷异,以及緩存管理
  • 異步下載圖片
  • 異步緩存(內(nèi)存+磁盤),并且自動(dòng)管理緩存有效性
  • 后臺(tái)圖片解壓縮
  • 同一個(gè) URL 不會(huì)重復(fù)下載
  • 自動(dòng)識(shí)別無(wú)效 URL锦聊,不會(huì)反復(fù)重試
  • 不阻塞主線程
  • 高性能
  • 使用 GCD 和 ARC
  • 支持多種圖片格式(包括 WebP 格式)
  • 支持動(dòng)圖(GIF)
    • 4.0 之前的動(dòng)圖效果并不是太好
    • 4.0 以后基于 FLAnimatedImage加載動(dòng)圖

注:本文選讀的代碼是 3.7.3 版本的歹嘹,所以動(dòng)圖加載還不支持 FLAnimatedImage

3. SDWebImage 與其他框架的對(duì)比

利益相關(guān):以下兩篇文章都是 SDWebImage 的維護(hù)者所寫括丁,具有一定的主觀性荞下,僅供參考。

4. 常見(jiàn)問(wèn)題

  • 問(wèn)題 1:使用 UITableViewCell 中的 imageView 加載不同尺寸的網(wǎng)絡(luò)圖片時(shí)會(huì)出現(xiàn)尺寸縮放問(wèn)題

    解決方案:自定義 UITableViewCell史飞,重寫 -layoutSubviews 方法尖昏,調(diào)整位置尺寸;或者直接棄用 UITableViewCellimageView构资,自己添加一個(gè) imageView 作為子控件抽诉。

  • 問(wèn)題 2:圖片刷新問(wèn)題:SDWebImage 在進(jìn)行緩存時(shí)忽略了所有服務(wù)器返回的 caching control 設(shè)置,并且在緩存時(shí)沒(méi)有做時(shí)間限制吐绵,這也就意味著圖片 URL 必須是靜態(tài)的了迹淌,要求服務(wù)器上一個(gè) URL 對(duì)應(yīng)的圖片內(nèi)容不允許更新。但是如果存儲(chǔ)圖片的服務(wù)器不由自己控制己单,也就是說(shuō) 圖片內(nèi)容更新了唉窃,URL 卻沒(méi)有更新,這種情況怎么辦纹笼?

    解決方案:在調(diào)用 sd_setImageWithURL: placeholderImage: options:方法時(shí)設(shè)置 options 參數(shù)為 SDWebImageRefreshCached纹份,這樣雖然會(huì)降低性能,但是下載圖片時(shí)會(huì)照顧到服務(wù)器返回的 caching control廷痘。

  • 問(wèn)題 3:在加載圖片時(shí)蔓涧,如何添加默認(rèn)的 progress indicator ?

    解決方案:在調(diào)用 -sd_setImageWithURL:方法之前笋额,先調(diào)用下面的方法:

     [imageView sd_setShowActivityIndicatorView:YES];
     [imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];
    

5. 用法

5.1 UITableView 中使用 UIImageView+WebCache

UITabelViewCell 中的 UIImageView 控件直接調(diào)用 sd_setImageWithURL: placeholderImage:方法即可

5.2 使用回調(diào) blocks

在 block 中得到圖片下載進(jìn)度和圖片加載完成(下載完成或者讀取緩存)的回調(diào)元暴,如果你在圖片加載完成前取消了請(qǐng)求操作,就不會(huì)收到成功或失敗的回調(diào)

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                      placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                             completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                    ... completion code here ...
                                 }];

5.3 SDWebImageManager 的使用

UIImageView(WebCache) 分類的核心在于 SDWebImageManager 的下載和緩存處理兄猩,SDWebImageManager將圖片下載和圖片緩存組合起來(lái)了茉盏。SDWebImageManager也可以單獨(dú)使用。

    SDWebImageManager *manager = [SDWebImageManager sharedManager];
    [manager loadImageWithURL:imageURL
                      options:0
                     progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                            // progression tracking code
                     }
                     completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                        if (image) {
                            // do something with image
                        }
                     }];

5.4 單獨(dú)使用 SDWebImageDownloader 異步下載圖片

我們還可以單獨(dú)使用 SDWebImageDownloader 來(lái)下載圖片枢冤,但是圖片內(nèi)容不會(huì)緩存援岩。

    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    [downloader downloadImageWithURL:imageURL
                             options:0
                            progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                // progression tracking code
                            }
                           completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                if (image && finished) {
                                    // do something with image
                                }
                            }];

5.5 單獨(dú)使用 SDImageCache 異步緩存圖片

SDImageCache 支持內(nèi)存緩存和異步的磁盤緩存(可選),如果你想單獨(dú)使用 SDImageCache 來(lái)緩存數(shù)據(jù)的話掏导,可以使用單例享怀,也可以創(chuàng)建一個(gè)有獨(dú)立命名空間的 SDImageCache 實(shí)例。

添加緩存的方法:

    [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

默認(rèn)情況下趟咆,圖片數(shù)據(jù)會(huì)同時(shí)緩存到內(nèi)存和磁盤中添瓷,如果你想只要內(nèi)存緩存的話梅屉,可以使用下面的方法:

    [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];

讀取緩存時(shí)可以使用 queryDiskCacheForKey:done: 方法,圖片緩存的 key 是唯一的鳞贷,通常就是圖片的 absolute URL坯汤。

    SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
    [imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
        // image is not nil if image was found
    }];

5.6 自定義緩存 key

有時(shí)候,一張圖片的 URL 中的一部分可能是動(dòng)態(tài)變化的(比如獲取權(quán)限上的限制)搀愧,所以我們只需要把 URL 中不變的部分作為緩存用的 key惰聂。

    SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
            url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
            return [url absoluteString];
        };

6. SDWebImage 4.0 遷移指南

按照版本號(hào)慣例(Semantic Versioning),從版本號(hào)可以看出 SDWebImage 4.0 是一個(gè)大版本咱筛,在結(jié)構(gòu)上和 API 方面都有所改動(dòng)搓幌。

除了 iOS 和 tvOS 之外,SDWebImage 4.0 還支持更多的平臺(tái)——watchOS 和 Max OS X迅箩。

借助 FLAnimatedImage 在動(dòng)圖支持上做了改進(jìn)溉愁,尤其是 GIF。

二饲趋、實(shí)現(xiàn)原理

1. 架構(gòu)圖(UML 類圖)

2. 流程圖(方法調(diào)用順序圖)

3. 目錄結(jié)構(gòu)

  • Downloader
    • SDWebImageDownloader
    • SDWebImageDownloaderOperation
  • Cache
    • SDImageCache
  • Utils
    • SDWebImageManager
    • SDWebImageDecoder
    • SDWebImagePrefetcher
  • Categories
    • UIView+WebCacheOperation
    • UIImageView+WebCache
    • UIImageView+HighlightedWebCache
    • UIButton+WebCache
    • MKAnnotationView+WebCache
    • NSData+ImageContentType
    • UIImage+GIF
    • UIImage+MultiFormat
    • UIImage+WebP
  • Other
    • SDWebImageOperation(協(xié)議)
    • SDWebImageCompat(宏定義拐揭、常量、通用函數(shù))
類名 功能
SDWebImageDownloader 是專門用來(lái)下載圖片和優(yōu)化圖片加載的奕塑,跟緩存沒(méi)有關(guān)系
SDWebImageDownloaderOperation 繼承于 NSOperation堂污,用來(lái)處理下載任務(wù)的
SDImageCache 用來(lái)處理內(nèi)存緩存和磁盤緩存(可選)的,其中磁盤緩存是異步進(jìn)行的龄砰,因此不會(huì)阻塞主線程
SDWebImageManager 作為 UIImageView+WebCache 背后的默默付出者敷鸦,主要功能是將圖片下載(SDWebImageDownloader)和圖片緩存(SDImageCache)兩個(gè)獨(dú)立的功能組合起來(lái)
SDWebImageDecoder 圖片解碼器,用于圖片下載完成后進(jìn)行解碼
SDWebImagePrefetcher 預(yù)下載圖片寝贡,方便后續(xù)使用,圖片下載的優(yōu)先級(jí)低值依,其內(nèi)部由 SDWebImageManager 來(lái)處理圖片下載和緩存
UIView+WebCacheOperation 用來(lái)記錄圖片加載的 operation圃泡,方便需要時(shí)取消和移除圖片加載的 operation
UIImageView+WebCache 集成 SDWebImageManager 的圖片下載和緩存功能到 UIImageView 的方法中,方便調(diào)用方的簡(jiǎn)單使用
UIImageView+HighlightedWebCache UIImageView+WebCache 類似愿险,也是包裝了 SDWebImageManager颇蜡,只不過(guò)是用于加載 highlighted 狀態(tài)的圖片
UIButton+WebCache UIImageView+WebCache 類似,集成 SDWebImageManager 的圖片下載和緩存功能到 UIButton 的方法中辆亏,方便調(diào)用方的簡(jiǎn)單使用
MKAnnotationView+WebCache UIImageView+WebCache 類似
NSData+ImageContentType 用于獲取圖片數(shù)據(jù)的格式(JPEG风秤、PNG等)
UIImage+GIF 用于加載 GIF 動(dòng)圖
UIImage+MultiFormat 根據(jù)不同格式的二進(jìn)制數(shù)據(jù)轉(zhuǎn)成 UIImage 對(duì)象
UIImage+WebP 用于解碼并加載 WebP 圖片

4. 核心邏輯

下載 Source code(3.7.3),運(yùn)行 pod install扮叨,然后打開 SDWebImage.xcworkspace缤弦,先 run 起來(lái)感受一下。

在了解細(xì)節(jié)之前我們先大概瀏覽一遍主流程彻磁,也就是最核心的邏輯碍沐。

我們從 MasterViewController 中的 [cell.imageView sd_setImageWithURL:url placeholderImage:placeholderImage]; 開始看起狸捅。

經(jīng)過(guò)層層調(diào)用,直到 UIImageView+WebCache 中最核心的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:累提。該方法中尘喝,主要做了以下幾件事:

  • 取消當(dāng)前正在進(jìn)行的加載任務(wù) operation
  • 設(shè)置 placeholder
  • 如果 URL 不為 nil,就通過(guò) SDWebImageManager 單例開啟圖片加載任務(wù) operation斋陪,SDWebImageManager 的圖片加載方法中會(huì)返回一個(gè) SDWebImageCombinedOperation 對(duì)象朽褪,這個(gè)對(duì)象包含一個(gè) cacheOperation 和一個(gè) cancelBlock。

SDWebImageManager 的圖片加載方法 downloadImageWithURL:options:progress:completed: 中會(huì)先拿圖片緩存的 key (這個(gè) key 默認(rèn)是圖片 URL)去 SDImageCache 單例中讀取內(nèi)存緩存无虚,如果有缔赠,就返回給 SDWebImageManager;如果內(nèi)存緩存沒(méi)有骑科,就開啟異步線程橡淑,拿經(jīng)過(guò) MD5 處理的 key 去讀取磁盤緩存,如果找到磁盤緩存了咆爽,就同步到內(nèi)存緩存中去梁棠,然后再返回給 SDWebImageManager

如果內(nèi)存緩存和磁盤緩存中都沒(méi)有斗埂,SDWebImageManager 就會(huì)調(diào)用 SDWebImageDownloader 單例的 -downloadImageWithURL: options: progress: completed: 方法去下載符糊,該會(huì)先將傳入的 progressBlockcompletedBlock 保存起來(lái),并在第一次下載該 URL 的圖片時(shí)呛凶,創(chuàng)建一個(gè) NSMutableURLRequest 對(duì)象和一個(gè) SDWebImageDownloaderOperation 對(duì)象男娄,并將該 SDWebImageDownloaderOperation 對(duì)象添加到 SDWebImageDownloaderdownloadQueue 來(lái)啟動(dòng)異步下載任務(wù)。

SDWebImageDownloaderOperation 中包裝了一個(gè) NSURLConnection 的網(wǎng)絡(luò)請(qǐng)求漾稀,并通過(guò) runloop 來(lái)保持 NSURLConnection 在 start 后模闲、收到響應(yīng)前不被干掉,下載圖片時(shí)崭捍,監(jiān)聽(tīng) NSURLConnection 回調(diào)的 -connection:didReceiveData: 方法中會(huì)負(fù)責(zé) progress 相關(guān)的處理和回調(diào)尸折,- connectionDidFinishLoading: 方法中會(huì)負(fù)責(zé)將 data 轉(zhuǎn)為 image,以及圖片解碼操作殷蛇,并最終回調(diào) completedBlock实夹。

SDWebImageDownloaderOperation 中的圖片下載請(qǐng)求完成后,會(huì)回調(diào)給 SDWebImageDownloader粒梦,然后 SDWebImageDownloader 再回調(diào)給 SDWebImageManager亮航,SDWebImageManager 中再將圖片分別緩存到內(nèi)存和磁盤上(可選),并回調(diào)給 UIImageView匀们,UIImageView 中再回到主線程設(shè)置 image 屬性缴淋。至此,圖片的下載和緩存操作就圓滿結(jié)束了。

當(dāng)然宴猾,SDWebImage 中還有很多細(xì)節(jié)可以深挖圆存,包括一些巧妙設(shè)計(jì)和知識(shí)點(diǎn),接下來(lái)再看看SDWebImage 中的實(shí)現(xiàn)細(xì)節(jié)仇哆。

三沦辙、實(shí)現(xiàn)細(xì)節(jié)

注:為了節(jié)省篇幅,這里使用偽代碼的方式來(lái)解讀讹剔,具體的閱讀注解見(jiàn) ShannonChenCHN/SDWebImage-3.7.3油讯。

從上面的核心邏輯分析可以看出,SDWebImage 最核心的功能也就是以下 4 件事:

  • 下載(SDWebImageDownloader
  • 緩存(SDImageCache
  • 將緩存和下載的功能組合起來(lái)(SDWebImageManager
  • 封裝成 UIImageView 等類的分類方法(UIImageView+WebCache 等)

1. 圖片下載

1.1 SDWebImageDownloader

SDWebImageDownloader 繼承于 NSObject延欠,主要承擔(dān)了異步下載圖片和優(yōu)化圖片加載的任務(wù)陌兑。

幾個(gè)問(wèn)題

  • 如何實(shí)現(xiàn)異步下載,也就是多張圖片同時(shí)下載由捎?
  • 如何處理同一張圖片(同一個(gè) URL)多次下載的情況兔综?

枚舉定義

// 下載選項(xiàng)
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    SDWebImageDownloaderHandleCookies = 1 << 5,
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    SDWebImageDownloaderHighPriority = 1 << 7,
};

// 下載任務(wù)執(zhí)行順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    SDWebImageDownloaderFIFOExecutionOrder, // 先進(jìn)先出
    SDWebImageDownloaderLIFOExecutionOrder  // 后進(jìn)先出
};

.h 文件中的屬性:

@property (assign, nonatomic) BOOL shouldDecompressImages;  // 下載完成后是否需要解壓縮圖片,默認(rèn)為 YES
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;

@property (strong, nonatomic) NSString *username;
@property (strong, nonatomic) NSString *password;
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;

.m 文件中的屬性:

@property (strong, nonatomic) NSOperationQueue *downloadQueue; // 圖片下載任務(wù)是放在這個(gè) NSOperationQueue 任務(wù)隊(duì)列中來(lái)管理的
@property (weak, nonatomic) NSOperation *lastAddedOperation;
@property (assign, nonatomic) Class operationClass;
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; // 圖片下載的回調(diào) block 都是存儲(chǔ)在這個(gè)屬性中狞玛,該屬性是一個(gè)字典软驰,key 是圖片的 URL,value 是一個(gè)數(shù)組心肪,包含每個(gè)圖片的多組回調(diào)信息锭亏。用 JSON 格式表示的話,就是下面這種形式:

.h 文件中方法

+ (SDWebImageDownloader *)sharedDownloader;

- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;

- (void)setOperationClass:(Class)operationClass; // 創(chuàng)建 operation 用的類

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
                                       
- (void)setSuspended:(BOOL)suspended;

.m 文件中的方法

// Lifecycle
+ (void)initialize;
+ (SDWebImageDownloader *)sharedDownloader;
- init;
- (void)dealloc;

// Setter and getter
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads;
- (NSUInteger)currentDownloadCount;
- (NSInteger)maxConcurrentDownloads;
- (void)setOperationClass:(Class)operationClass;

// Download
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
          andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                     forURL:(NSURL *)url
             createCallback:(SDWebImageNoParamsBlock)createCallback;

// Download queue            
- (void)setSuspended:(BOOL)suspended;

具體實(shí)現(xiàn):

先看看 +initialize 方法硬鞍,這個(gè)方法中主要是通過(guò)注冊(cè)通知 讓SDNetworkActivityIndicator 監(jiān)聽(tīng)下載事件慧瘤,來(lái)顯示和隱藏狀態(tài)欄上的 network activity indicator。為了讓 SDNetworkActivityIndicator 文件可以不用導(dǎo)入項(xiàng)目中來(lái)(如果不要的話)固该,這里使用了 runtime 的方式來(lái)實(shí)現(xiàn)動(dòng)態(tài)創(chuàng)建類以及調(diào)用方法锅减。

+ (void)initialize {
    if (NSClassFromString(@"SDNetworkActivityIndicator")) {
        id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
        
        # 先移除通知觀察者 SDNetworkActivityIndicator
        # 再添加通知觀察者 SDNetworkActivityIndicator
    }
}

+sharedDownloader 方法中調(diào)用了 -init 方法來(lái)創(chuàng)建一個(gè)單例,-init方法中做了一些初始化設(shè)置和默認(rèn)值設(shè)置,包括設(shè)置最大并發(fā)數(shù)(6)、下載超時(shí)時(shí)長(zhǎng)(15s)等藏斩。

- (id)init {
    #設(shè)置下載 operation 的默認(rèn)執(zhí)行順序(先進(jìn)先出還是先進(jìn)后出)
    #初始化 _downloadQueue(下載隊(duì)列),_URLCallbacks(下載回調(diào) block 的容器),_barrierQueue(GCD 隊(duì)列)
    #設(shè)置 _downloadQueue 的隊(duì)列最大并發(fā)數(shù)默認(rèn)值為 6
    #設(shè)置 _HTTPHeaders 默認(rèn)值 
    #設(shè)置默認(rèn)下載超時(shí)時(shí)長(zhǎng) 15s 
    ...
}

除了以上兩個(gè)方法之外拴疤,這個(gè)類中最核心的方法就是 - downloadImageWithURL: options: progress: completed: 方法永部,這個(gè)方法中首先通過(guò)調(diào)用 -addProgressCallback: andCompletedBlock: forURL: createCallback: 方法來(lái)保存每個(gè) url 對(duì)應(yīng)的回調(diào) block,-addProgressCallback: ... 方法先進(jìn)行錯(cuò)誤檢查呐矾,判斷 URL 是否為空苔埋,然后再將 URL 對(duì)應(yīng)的 progressBlockcompletedBlock 保存到 URLCallbacks 屬性中去。

URLCallbacks 屬性是一個(gè) NSMutableDictionary 對(duì)象蜒犯,key 是圖片的 URL组橄,value 是一個(gè)數(shù)組荞膘,包含每個(gè)圖片的多組回調(diào)信息。用 JSON 格式表示的話玉工,就是下面這種形式:

{
    "callbacksForUrl1": [
        {
            "kProgressCallbackKey": "progressCallback1_1",
            "kCompletedCallbackKey": "completedCallback1_1"
        },
        {
            "kProgressCallbackKey": "progressCallback1_2",
            "kCompletedCallbackKey": "completedCallback1_2"
        }
    ],
    "callbacksForUrl2": [
        {
            "kProgressCallbackKey": "progressCallback2_1",
            "kCompletedCallbackKey": "completedCallback2_1"
        },
        {
            "kProgressCallbackKey": "progressCallback2_2",
            "kCompletedCallbackKey": "completedCallback2_2"
        }
    ]
}

這里有個(gè)細(xì)節(jié)需要注意羽资,因?yàn)榭赡芡瑫r(shí)下載多張圖片,所以就可能出現(xiàn)多個(gè)線程同時(shí)訪問(wèn) URLCallbacks 屬性的情況遵班。為了保證線程安全屠升,所以這里使用了 dispatch_barrier_sync 來(lái)分步執(zhí)行添加到 barrierQueue 中的任務(wù),這樣就能保證同一時(shí)間只有一個(gè)線程能對(duì) URLCallbacks 進(jìn)行操作狭郑。

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    #1. 判斷 url 是否為 nil腹暖,如果為 nil 則直接回調(diào) completedBlock,返回失敗的結(jié)果翰萨,然后 return脏答,因?yàn)?url 會(huì)作為存儲(chǔ) callbacks 的 key
    
    #2. 處理同一個(gè) URL 的多次下載請(qǐng)求(MARK: 使用 dispatch_barrier_sync 函數(shù)來(lái)保證同一時(shí)間只有一個(gè)線程能對(duì) URLCallbacks 進(jìn)行操作):
      ## 從屬性 URLCallbacks(一個(gè)字典) 中取出對(duì)應(yīng) url 的 callBacksForURL(這是一個(gè)數(shù)組,因?yàn)榭赡芤粋€(gè) url 不止在一個(gè)地方下載)
      ## 如果沒(méi)有取到亩鬼,也就意味著這個(gè) url 是第一次下載殖告,那就初始化一個(gè) callBacksForURL 放到屬性 URLCallbacks 中
      ## 往數(shù)組 callBacksForURL 中添加 包裝有 callbacks(progressBlock 和 completedBlock)的字典
      ## 更新 URLCallbacks 存儲(chǔ)的對(duì)應(yīng) url 的 callBacksForURL
      
    #3. 如果這個(gè) url 是第一次請(qǐng)求下載,就回調(diào) createCallback

}


如果這個(gè) URL 是第一次被下載辛孵,就要回調(diào) createCallback丛肮,createCallback 主要做的就是創(chuàng)建并開啟下載任務(wù),下面是 createCallback 的具體實(shí)現(xiàn)邏輯:


- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
     #1. 調(diào)用 - [SDWebImageDownloader addProgressCallback: andCompletedBlock: forURL: createCallback: ] 方法魄缚,直接把入?yún)?url宝与、progressBlock 和 completedBlock 傳進(jìn)該方法,并在第一次下載該 URL 時(shí)回調(diào) createCallback
     
        ## createCallback 的回調(diào)處理:{
          1.1 創(chuàng)建下載 request 冶匹,設(shè)置 request 的 cachePolicy习劫、HTTPShouldHandleCookies、HTTPShouldUsePipelining嚼隘,以及 allHTTPHeaderFields(這個(gè)屬性交由外面處理诽里,設(shè)計(jì)的比較巧妙)

          1.2 創(chuàng)建 SDWebImageDownloaderOperation(繼承自 NSOperation)

              ### 1.2.1 SDWebImageDownloaderOperation 的 progressBlock 回調(diào)處理 {
                      (這個(gè) block 有兩個(gè)回調(diào)參數(shù):接收到的數(shù)據(jù)大小和預(yù)計(jì)數(shù)據(jù)大小)
                      這里用了 weak-strong dance
                      首先使用 strongSelf 強(qiáng)引用 weakSelf飞蛹,目的是為了保住 self 不被釋放  
                      然后檢查 self 是否已經(jīng)被釋放(這里為什么先“卑疲活”后“判空”呢?因?yàn)槿绻扰锌盏脑捨蚤埽锌赡芘锌蘸?self 就被釋放了)
                      取出 url 對(duì)應(yīng)的回調(diào) block 數(shù)組(這里取的時(shí)候有些講究墓懂,考慮了多線程問(wèn)題,而且取的是 copy 的內(nèi)容)
                      遍歷數(shù)組霉囚,從每個(gè)元素(字典)中取出 progressBlock 進(jìn)行回調(diào)    
                   }
              ### 1.2.2 SDWebImageDownloaderOperation 的 completedBlock 回調(diào)處理 {
                       (這個(gè) block 有四個(gè)回調(diào)參數(shù):圖片 UIImage捕仔,圖片數(shù)據(jù) NSData,錯(cuò)誤 NSError,是否結(jié)束 isFinished)
                       同樣榜跌,這里也用了 weak-strong dance
                       接著闪唆,取出 url 對(duì)應(yīng)的回調(diào) block 數(shù)組
                       如果結(jié)束了(isFinished),就移除 url 對(duì)應(yīng)的回調(diào) block 數(shù)組(移除的時(shí)候也要考慮多線程問(wèn)題)
                       遍歷數(shù)組钓葫,從每個(gè)元素(字典)中取出 completedBlock 進(jìn)行回調(diào) 
              }
              ### SDWebImageDownloaderOperation 的 cancelBlock 回調(diào)處理 {
                       同樣悄蕾,這里也用了 weak-strong dance
                       然后移除 url 對(duì)應(yīng)的所有回調(diào) block
              }
          1.3 設(shè)置下載完成后是否需要解壓縮
          1.4 如果設(shè)置了 username 和 password,就給 operation 的下載請(qǐng)求設(shè)置一個(gè) NSURLCredential  
          1.5 設(shè)置 operation 的隊(duì)列優(yōu)先級(jí)
          1.6 將 operation 加入到隊(duì)列 downloadQueue 中瓤逼,隊(duì)列(NSOperationQueue)會(huì)自動(dòng)管理 operation 的執(zhí)行
          1.7 如果 operation 執(zhí)行順序是先進(jìn)后出笼吟,就設(shè)置 operation 依賴關(guān)系(先加入的依賴于后加入的),并記錄最后一個(gè) operation(lastAddedOperation)
        }

     #2. 返回 createCallback 中創(chuàng)建的 operation(SDWebImageDownloaderOperation)
}

createCallback 方法中調(diào)用了 - [SDWebImageDownloaderOperation initWithRequest: options: progress:] 方法來(lái)創(chuàng)建下載任務(wù) SDWebImageDownloaderOperation霸旗。那么贷帮,這個(gè) SDWebImageDownloaderOperation 類究竟是干什么的呢?下一節(jié)再看诱告。

知識(shí)點(diǎn)

  1. SDWebImageDownloaderOptions 枚舉使用了位運(yùn)算
    應(yīng)用:通過(guò)“與”運(yùn)算符撵枢,可以判斷是否設(shè)置了某個(gè)枚舉選項(xiàng),因?yàn)槊總€(gè)枚舉選擇項(xiàng)中只有一位是1精居,其余位都是 0锄禽,所以只有參與運(yùn)算的另一個(gè)二進(jìn)制值在同樣的位置上也為 1,與 運(yùn)算的結(jié)果才不會(huì)為 0.
      0101 (相當(dāng)于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache)
    & 0100 (= 1 << 2靴姿,也就是 SDWebImageDownloaderUseNSURLCache)
    = 0100 (> 0沃但,也就意味著 option 參數(shù)中設(shè)置了 SDWebImageDownloaderUseNSURLCache)
    
  2. dispatch_barrier_sync 函數(shù)的使用
  3. weak-strong dance
  4. HTTP header 的理解
  5. NSOperationQueue 的使用
  6. NSURLRequestcachePolicyHTTPShouldHandleCookies佛吓、HTTPShouldUsePipelining
  7. NSURLCredential
  8. createCallback 里面為什么要用 wself宵晚?
    NSTimeInterval timeoutInterval = wself.downloadTimeout;
    

1.2 SDWebImageDownloaderOperation

每張圖片的下載都會(huì)發(fā)出一個(gè)異步的 HTTP 請(qǐng)求,這個(gè)請(qǐng)求就是由 SDWebImageDownloaderOperation 管理的维雇。

SDWebImageDownloaderOperation 繼承 NSOperation淤刃,遵守 SDWebImageOperationNSURLConnectionDataDelegate 協(xié)議吱型。

SDWebImageOperation 協(xié)議只定義了一個(gè)方法 -cancel逸贾,用來(lái)取消 operation。

幾個(gè)問(wèn)題

  • 如何實(shí)現(xiàn)下載的網(wǎng)絡(luò)請(qǐng)求津滞?
  • 如何管理整個(gè)圖片下載的過(guò)程铝侵?
  • 圖片下載完成后需要做哪些處理?

.h 文件中的屬性:

@property (strong, nonatomic, readonly) NSURLRequest *request; // 用來(lái)給 operation 中的 connection 使用的請(qǐng)求
@property (assign, nonatomic) BOOL shouldDecompressImages; // 下載完成后是否需要解壓縮
@property (nonatomic, assign) BOOL shouldUseCredentialStorage; 
@property (nonatomic, strong) NSURLCredential *credential;
@property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;
@property (assign, nonatomic) NSInteger expectedSize;
@property (strong, nonatomic) NSURLResponse *response;

其他繼承自 NSOperation 的屬性(略)

.m 文件中的屬性:

@property (copy, nonatomic) SDWebImageDownloaderProgressBlock progressBlock;    
@property (copy, nonatomic) SDWebImageDownloaderCompletedBlock completedBlock;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;

@property (assign, nonatomic, getter = isExecuting) BOOL executing; // 覆蓋了 NSOperation 的 executing
@property (assign, nonatomic, getter = isFinished) BOOL finished;  // 覆蓋了 NSOperation 的 finished
@property (assign, nonatomic) NSInteger expectedSize;
@property (strong, nonatomic) NSMutableData *imageData;
@property (strong, nonatomic) NSURLConnection *connection;
@property (strong, atomic) NSThread *thread;

@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; // Xcode 的 BaseSDK 設(shè)置為 iOS 4.0 時(shí)以上使用

// 成員變量
size_t width, height;               // 圖片寬高
UIImageOrientation orientation;     // 圖片方向
BOOL responseFromCached;

.h 文件中的方法:

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;    

其他繼承自 NSOperation 的方法(略)

.m 文件中的方法:

// 覆蓋了父類的屬性触徐,需要重新實(shí)現(xiàn)屬性合成方法
@synthesize executing = _executing;
@synthesize finished = _finished;

// Initialization
- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;
// Operation
- (void)start;
- (void)cancel;
- (void)cancelInternalAndStop;
- (void)cancelInternal;
- (void)done;
- (void)reset;

// Setter and getter
- (void)setFinished:(BOOL)finished; 
- (void)setExecuting:(BOOL)executing; 
- (BOOL)isConcurrent; 

// NSURLConnectionDataDelegate 方法
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; //  下載過(guò)程中的 response 回調(diào)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; // 下載過(guò)程中 data 回調(diào)
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection; // 下載完成時(shí)回調(diào)
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; // 下載失敗時(shí)回調(diào)
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; // 在 connection 存儲(chǔ) cached response 到緩存中之前調(diào)用
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection; //  URL loader 是否應(yīng)該使用 credential storage
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; // connection 發(fā)送身份認(rèn)證的請(qǐng)求之前被調(diào)用

// Helper
+ (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value;
- (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image;
- (BOOL)shouldContinueWhenAppEntersBackground;

具體實(shí)現(xiàn):

首先來(lái)看看指定初始化方法 -initWithRequest:options:progress:completed:cancelled:咪鲜,這個(gè)方法是保存一些傳入的參數(shù),設(shè)置一些屬性的初始默認(rèn)值锌介。

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock {
    # 接受參數(shù)嗜诀,設(shè)置屬性
    # 設(shè)置屬性_shouldUseCredentialStorage、_executing孔祸、_finished隆敢、_expectedSize、responseFromCached 的默認(rèn)值/初始值
}

當(dāng)創(chuàng)建的 SDWebImageDownloaderOperation 對(duì)象被加入到 downloader 的 downloadQueue 中時(shí)崔慧,該對(duì)象的 -start 方法就會(huì)被自動(dòng)調(diào)用拂蝎。
-start 方法中首先創(chuàng)建了用來(lái)下載圖片數(shù)據(jù)的 NSURLConnection,然后開啟 connection惶室,同時(shí)發(fā)出開始圖片下載的 SDWebImageDownloadStartNotification 通知温自,為了防止非主線程的請(qǐng)求被 kill 掉,這里開啟 runloop 被食活悼泌,直到請(qǐng)求返回。

- (void)start {
    # 給 `self` 加鎖 {
        ## 如果 `self` 被 cancell 掉的話夹界,finished 屬性變?yōu)?YES馆里,reset 下載數(shù)據(jù)和回調(diào) block,然后直接 return可柿。

        ## 如果允許程序退到后臺(tái)后繼續(xù)下載鸠踪,就標(biāo)記為允許后臺(tái)執(zhí)行,在后臺(tái)任務(wù)過(guò)期的回調(diào) block 中 {
            首先來(lái)一個(gè) weak-strong dance
            調(diào)用 cancel 方法(這個(gè)方法里面又做了一些處理复斥,反正就是 cancel 掉當(dāng)前的 operation)
            調(diào)用 UIApplication 的 endBackgroundTask: 方法結(jié)束任務(wù)
            記錄結(jié)束后的 taskId
            
        }
        
        ## 標(biāo)記 executing 屬性為 YES
        ## 創(chuàng)建 connection营密,賦值給 connection 屬性
        ## 獲取 currentThread,賦值給 thread 屬性

    }
    
    # 啟動(dòng) connection
    # 因?yàn)樯厦娉跏蓟?connection 時(shí)可能會(huì)失敗目锭,所以這里我們需要根據(jù)不同情況做處理
        ## A.如果 connection 不為 nil
            ### 回調(diào) progressBlock(初始的 receivedSize 為 0评汰,expectSize 為 -1)
            ### 發(fā)出 SDWebImageDownloadStartNotification 通知(SDWebImageDownloader 會(huì)監(jiān)聽(tīng)到)
            ### 開啟 runloop
            ### runloop 結(jié)束后繼續(xù)往下執(zhí)行(也就是 cancel 掉或者 NSURLConnection 請(qǐng)求完畢代理回調(diào)后調(diào)用了 CFRunLoopStop)
        
        ## B.如果 connection 為 nil,回調(diào) completedBlock侣集,返回 connection 初始化失敗的錯(cuò)誤信息
    # 下載完成后键俱,調(diào)用 endBackgroundTask: 標(biāo)記后臺(tái)任務(wù)結(jié)束
}

NSURLConnection 請(qǐng)求圖片數(shù)據(jù)時(shí),服務(wù)器返回的的結(jié)果是通過(guò) NSURLConnectionDataDelegate 的代理方法回調(diào)的世分,其中最主要的是以下三個(gè)方法:

- connection:didReceiveResponse: //  下載過(guò)程中的 response 回調(diào)编振,調(diào)用一次
- connection:didReceiveData:     // 下載過(guò)程中 data 回調(diào),調(diào)用多次
- connectionDidFinishLoading:    // 下載完成時(shí)回調(diào)臭埋,調(diào)用一次

前兩個(gè)方法是在下載過(guò)程中回調(diào)的踪央,第三個(gè)方法是在下載完成時(shí)回調(diào)的。第一個(gè)方法 - connection:didReceiveResponse: 被調(diào)用后瓢阴,接著會(huì)多次調(diào)用 - connection:didReceiveData: 方法來(lái)更新進(jìn)度畅蹂、拼接圖片數(shù)據(jù),當(dāng)圖片數(shù)據(jù)全部下載完成時(shí)荣恐,- connectionDidFinishLoading: 方法就會(huì)被調(diào)用液斜。

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    #A. 返回 code 不是 304 Not Modified
        1. 獲取 expectedSize累贤,回調(diào) progressBlock
        2. 初始化 imageData 屬性
        3. 發(fā)送 SDWebImageDownloadReceiveResponseNotification 通知
    #B. 針對(duì) 304 Not Modified 做處理,直接 cancel operation少漆,并返回緩存的 image
        1. 取消連接
        2. 發(fā)送 SDWebImageDownloadStopNotification 通知
        3. 回調(diào) completedBlock
        4. 停止 runloop
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    # 1.拼接圖片數(shù)據(jù)
    # 2.針對(duì) `SDWebImageDownloaderProgressiveDownload` 做的處理
        ## 2.1 根據(jù)更新的 imageData 創(chuàng)建 CGImageSourceRef 對(duì)象
        ## 2.2 首次獲取到數(shù)據(jù)時(shí)臼膏,讀取圖片屬性:width, height, orientation
        ## 2.3 圖片還沒(méi)下載完,但不是第一次拿到數(shù)據(jù)示损,使用現(xiàn)有圖片數(shù)據(jù) CGImageSourceRef 創(chuàng)建 CGImageRef 對(duì)象
        ## 2.4 對(duì)圖片進(jìn)行縮放渗磅、解碼,回調(diào) completedBlock
    # 3.回調(diào) progressBlock
}

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
    # 1. 下載結(jié)束检访,停止 runloop始鱼,發(fā)送 SDWebImageDownloadStopNotification 通知和 SDWebImageDownloadFinishNotification 通知
    # 2. 回調(diào) completionBlock
        # 2.1 如果是返回的結(jié)果是 URL Cache,就回調(diào)圖片數(shù)據(jù)為 nil 的 completionBlock
        # 2.2 如果有圖片數(shù)據(jù)
            # 2.2.1 針對(duì)不同圖片格式進(jìn)行數(shù)據(jù)轉(zhuǎn)換 data -> image
            # 2.2.2 據(jù)圖片名中是否帶 @2x 和 @3x 來(lái)做 scale 處理
            # 2.2.3 如果需要解碼脆贵,就進(jìn)行圖片解碼(如果不是 GIF 圖)
            # 2.2.4 判斷圖片尺寸是否為空医清,并回調(diào) completionBlock
        # 2.3 如果沒(méi)有圖片數(shù)據(jù),回調(diào)帶有錯(cuò)誤信息的 completionBlock
    # 3. 將 completionBlock 置為 nil
    # 4. 重置
}


當(dāng)圖片的所有數(shù)據(jù)下載完成后卖氨,SDWebImageDownloader 傳入的 completionBlock 被調(diào)用状勤,至此,整個(gè)圖片的下載過(guò)程就結(jié)束了双泪。從上面的解讀中我們可以看到持搜,一張圖片的數(shù)據(jù)下載是由一個(gè) NSConnection 對(duì)象來(lái)完成的,這個(gè)對(duì)象的整個(gè)生命周期(從創(chuàng)建到下載結(jié)束)又是由 SDWebImageDownloaderOperation 來(lái)控制的焙矛,將 operation 加入到 operation queue 中就可以實(shí)現(xiàn)多張圖片同時(shí)下載了葫盼。

簡(jiǎn)單概括成一句話就是,NSConnection 負(fù)責(zé)網(wǎng)絡(luò)請(qǐng)求村斟,NSOperation 負(fù)責(zé)多線程贫导。

知識(shí)點(diǎn)

  1. NSOperation-start 方法、-main 方法和 -cancel 方法
  2. -start 方法中為什么要調(diào)用 CFRunLoopRun() 或者 CFRunLoopRunInMode() 函數(shù)蟆盹?

參考:

  1. SDWebImageDownloaderOperation 中是什么時(shí)候開啟異步線程的孩灯?

  2. NSURLConnection 的幾個(gè)代理方法分別在什么時(shí)候調(diào)用?

  3. NSURLCache 是什么逾滥?

  4. 下載完成后峰档,為什么需要對(duì)圖片進(jìn)行解壓縮操作?

  5. WebP 圖片的解碼

2. 圖片緩存——SDImageCache

首先我們想一想寨昙,為什么需要緩存讥巡?

  • 以空間換時(shí)間,提升用戶體驗(yàn):加載同一張圖片舔哪,讀取緩存是肯定比遠(yuǎn)程下載的速度要快得多的
  • 減少不必要的網(wǎng)絡(luò)請(qǐng)求欢顷,提升性能,節(jié)省流量:一般來(lái)講捉蚤,同一張圖片的 URL 是不會(huì)經(jīng)常變化的抬驴,所以沒(méi)有必要重復(fù)下載炼七。另外,現(xiàn)在的手機(jī)存儲(chǔ)空間都比較大布持,相對(duì)于流量來(lái)特石,緩存占的那點(diǎn)空間算不了什么

SDImageCache 管理著一個(gè)內(nèi)存緩存和磁盤緩存(可選),同時(shí)在寫入磁盤緩存時(shí)采取異步執(zhí)行鳖链,所以不會(huì)阻塞主線程,影響用戶體驗(yàn)墩莫。

幾個(gè)問(wèn)題

  • 從讀取速度和保存時(shí)間上來(lái)考慮芙委,緩存該怎么存?key 怎么定狂秦?
  • 內(nèi)存緩存怎么存灌侣?
  • 磁盤緩存怎么存?路徑裂问、文件名怎么定侧啼?
  • 使用時(shí)怎么讀取緩存?
  • 什么時(shí)候需要移除緩存堪簿?怎么移除痊乾?

枚舉

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    SDImageCacheTypeNone,   // 沒(méi)有讀取到圖片緩存,需要從網(wǎng)上下載
    SDImageCacheTypeDisk,   // 磁盤中的緩存
    SDImageCacheTypeMemory  // 內(nèi)存中的緩存
};

.h 文件中的屬性:

@property (assign, nonatomic) BOOL shouldDecompressImages;  // 讀取磁盤緩存后椭更,是否需要對(duì)圖片進(jìn)行解壓縮

@property (assign, nonatomic) NSUInteger maxMemoryCost; // 其實(shí)就是 NSCache 的 totalCostLimit哪审,內(nèi)存緩存總消耗的最大限制,cost 是根據(jù)內(nèi)存中的圖片的像素大小來(lái)計(jì)算的
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit; // 其實(shí)就是 NSCache 的 countLimit虑瀑,內(nèi)存緩存的最大數(shù)目

@property (assign, nonatomic) NSInteger maxCacheAge;    // 磁盤緩存的最大時(shí)長(zhǎng)湿滓,也就是說(shuō)緩存存多久后需要?jiǎng)h掉
@property (assign, nonatomic) NSUInteger maxCacheSize;  // 磁盤緩存文件總體積最大限制,以 bytes 來(lái)計(jì)算

.m 文件中的屬性:

@property (strong, nonatomic) NSCache *memCache;
@property (strong, nonatomic) NSString *diskCachePath;
@property (strong, nonatomic) NSMutableArray *customPaths; // // 只讀的路徑舌狗,比如 bundle 中的文件路徑叽奥,用來(lái)在 SDWebImage 下載、讀取緩存之前預(yù)加載用的
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;

NSFileManager *_fileManager;

.h 文件中的方法:

+ (SDImageCache *)sharedImageCache;
- (id)initWithNamespace:(NSString *)ns;
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;

-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;


- (void)addReadOnlyCachePath:(NSString *)path;


- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;


- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

- (void)clearMemory;

- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;


- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;


- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;


- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
- (NSString *)defaultCachePathForKey:(NSString *)key;

.m 文件中的方法和函數(shù):

  1. 方法
// Lifecycle
+ (SDImageCache *)sharedImageCache;
- (id)init;
- (id)initWithNamespace:(NSString *)ns;
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
- (void)dealloc;

// Cache Path
- (void)addReadOnlyCachePath:(NSString *)path; // 添加只讀路徑,比如 bundle 中的文件路徑抬闷,用來(lái)在 SDWebImage 下載垂寥、讀取緩存之前預(yù)加載用的
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
- (NSString *)defaultCachePathForKey:(NSString *)key;
- (NSString *)cachedFileNameForKey:(NSString *)key
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;

// Store Image
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk 
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;


// Check if image exists
- (BOOL)diskImageExistsWithKey:(NSString *)key;
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;

// Query the image cache
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key;
- (UIImage *)diskImageForKey:(NSString *)key;
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image;

// Remove specified image
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

// Setter and getter
- (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost;
- (NSUInteger)maxMemoryCost;
- (NSUInteger)maxMemoryCountLimit;
- (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit;

// Clear and clean
- (void)clearMemory;
- (void)clearDisk;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)cleanDisk;
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)backgroundCleanDisk;

// Cache Size
- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

2.函數(shù)

NSUInteger SDCacheCostForImage(UIImage *image);
BOOL ImageDataHasPNGPreffix(NSData *data);

具體實(shí)現(xiàn):

SDImageCache 的內(nèi)存緩存是通過(guò)一個(gè)繼承 NSCacheAutoPurgeCache 類來(lái)實(shí)現(xiàn)的,NSCache 是一個(gè)類似于 NSMutableDictionary 存儲(chǔ) key-value 的容器膀篮,主要有以下幾個(gè)特點(diǎn):

  • 自動(dòng)刪除機(jī)制:當(dāng)系統(tǒng)內(nèi)存緊張時(shí),NSCache會(huì)自動(dòng)刪除一些緩存對(duì)象
  • 線程安全:從不同線程中對(duì)同一個(gè) NSCache 對(duì)象進(jìn)行增刪改查時(shí)岂膳,不需要加鎖
  • 不同于 NSMutableDictionary誓竿,NSCache存儲(chǔ)對(duì)象時(shí)不會(huì)對(duì) key 進(jìn)行 copy 操作

SDImageCache 的磁盤緩存是通過(guò)異步操作 NSFileManager 存儲(chǔ)緩存文件到沙盒來(lái)實(shí)現(xiàn)的。

  1. 初始化

-init 方法中默認(rèn)調(diào)用了 -initWithNamespace: 方法谈截,-initWithNamespace: 方法又調(diào)用了 -makeDiskCachePath: 方法來(lái)初始化緩存目錄路徑筷屡, 同時(shí)還調(diào)用了 -initWithNamespace:diskCacheDirectory: 方法來(lái)實(shí)現(xiàn)初始化涧偷。下面是初始化方法調(diào)用棧:

-init
    -initWithNamespace:
        -makeDiskCachePath:
        -initWithNamespace:diskCacheDirectory:

-initWithNamespace:diskCacheDirectory: 是一個(gè) Designated Initializer,這個(gè)方法中主要是初始化實(shí)例變量毙死、屬性燎潮,設(shè)置屬性默認(rèn)值,并根據(jù) namespace 設(shè)置完整的緩存目錄路徑扼倘,除此之外确封,還針對(duì) iOS 添加了通知觀察者,用于內(nèi)存緊張時(shí)清空內(nèi)存緩存再菊,以及程序終止運(yùn)行時(shí)和程序退到后臺(tái)時(shí)清掃磁盤緩存爪喘。

  1. 寫入緩存

寫入緩存的操作主要是由 - storeImage:recalculateFromImage:imageData:forKey:toDisk: 方法處理的,在存儲(chǔ)緩存數(shù)據(jù)時(shí)纠拔,先計(jì)算圖片像素大小秉剑,并存儲(chǔ)到內(nèi)存緩存中去,然后如果需要存到磁盤(沙盒)中稠诲,就開啟異步線程將圖片的二進(jìn)制數(shù)據(jù)存儲(chǔ)到磁盤(沙盒)中侦鹏。

如果需要在存儲(chǔ)之前將傳進(jìn)來(lái)的 image 轉(zhuǎn)成 NSData,而不是直接使用傳入的 imageData臀叙,那么就要針對(duì) iOS 系統(tǒng)下略水,按不同的圖片格式來(lái)轉(zhuǎn)成對(duì)應(yīng)的 NSData 對(duì)象。那么圖片格式是怎么判斷的呢劝萤?這里是根據(jù)是否有 alpha 通道以及圖片數(shù)據(jù)的前 8 位字節(jié)來(lái)判斷是不是 PNG 圖片聚请,不是 PNG 的話就按照 JPG 來(lái)處理。

將圖片數(shù)據(jù)存儲(chǔ)到磁盤(沙盒)時(shí)稳其,需要提供一個(gè)包含文件名的路徑驶赏,這個(gè)文件名是一個(gè)對(duì) key 進(jìn)行 MD5 處理后生成的字符串。

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    # 1. 添加內(nèi)存緩存
        # 1.1 計(jì)算圖片像素大小
        # 1.2 將 image 存入 memCache 中

    # 2. 如果需要存儲(chǔ)到沙盒的話既鞠,就異步執(zhí)行磁盤緩存操作
        # 2.1 如果需要 recalculate (重新轉(zhuǎn) data)或者傳進(jìn)來(lái)的 imageData 為空的話煤傍,就再轉(zhuǎn)一次 data,因?yàn)榇鏋槲募谋仨毷嵌M(jìn)制數(shù)據(jù)
            # 2.1.1 如果 imageData 為 nil嘱蛋,就根據(jù) image 是否有 alpha 通道來(lái)判斷圖片是否是 PNG 格式的
            # 2.1.2 如果 imageData 不為 nil蚯姆,就根據(jù) imageData 的前 8 位字節(jié)來(lái)判斷是不是 PNG 格式的,因?yàn)?PNG 圖片有一個(gè)唯一簽名洒敏,前 8 位字節(jié)是(十進(jìn)制): 137 80 78 71 13 10 26 10
            # 2.1.3 根據(jù)圖片格式將 UIImage 轉(zhuǎn)為對(duì)應(yīng)的二進(jìn)制數(shù)據(jù) NSData

        # 2.2 借助 NSFileManager 將圖片二進(jìn)制數(shù)據(jù)存儲(chǔ)到沙盒龄恋,存儲(chǔ)的文件名是對(duì) key 進(jìn)行 MD5 處理后生成的字符串

}

3.讀取緩存

SDWebImage 在給 UIImageView 加載圖片時(shí)首先需要查詢緩存,查詢緩存的操作主要是 -queryDiskCacheForKey:done: 方法來(lái)實(shí)現(xiàn)的凶伙,該方法首先會(huì)調(diào)用 -imageFromMemoryCacheForKey 方法來(lái)查詢內(nèi)存緩存郭毕,也就是從 memCache 中去找,如果找到了對(duì)應(yīng)的圖片(一個(gè) UIImage 對(duì)象)函荣,就直接回調(diào) doneBlock显押,并直接返回扳肛。 如果內(nèi)存緩存中沒(méi)有找到對(duì)應(yīng)的圖片,就開啟異步隊(duì)列乘碑,調(diào)用 -diskImageForKey 讀取磁盤緩存挖息,讀取成功之后,再保存到內(nèi)存緩存兽肤,最后再回到主隊(duì)列套腹,回調(diào) doneBlock

其中讀取磁盤緩存并不是一步就完成了的资铡,讀取磁盤緩存時(shí)电禀,會(huì)先從沙盒中去找,如果沙盒中沒(méi)有害驹,再?gòu)?customPaths (也就是 bundle)中去找,找到之后蛤育,再對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換宛官,后面的圖片處理步驟跟圖片下載成功后的圖片處理步驟一樣——先將 data 轉(zhuǎn)成 image,再進(jìn)行根據(jù)文件名中的 @2x瓦糕、@3x 進(jìn)行縮放處理底洗,如果需要解壓縮,最后再解壓縮一下咕娄。

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    # 1.先檢查內(nèi)存緩存亥揖,如果找到了就回調(diào) doneBlock,并直接返回
    
    # 2.開啟異步隊(duì)列圣勒,讀取硬盤緩存
        # 2.1 讀取磁盤緩存
        # 2.2 如果有磁盤緩存费变,就保存到內(nèi)存緩存
        # 2.3 回到主隊(duì)列,回調(diào) doneBlock
}

4.清掃磁盤緩存

每新加載一張圖片圣贸,就會(huì)新增一份緩存挚歧,時(shí)間一長(zhǎng),磁盤上的緩存只會(huì)越來(lái)越多吁峻,所以我們需要定期清除部分緩存滑负。值得注意的是,清掃磁盤緩存(clean)和清空磁盤緩存(clear)是兩個(gè)不同的概念用含,清空是刪除整個(gè)緩存目錄矮慕,清掃只是刪除部分緩存文件。

清掃磁盤緩存有兩個(gè)指標(biāo):一是緩存有效期啄骇,二是緩存體積最大限制痴鳄。SDImageCache中的緩存有效期是通過(guò) maxCacheAge 屬性來(lái)設(shè)置的,默認(rèn)值是 1 周缸夹,緩存體積最大限制是通過(guò) maxCacheSize 來(lái)設(shè)置的夏跷,默認(rèn)值為 0哼转。

SDImageCache 在初始化時(shí)添加了通知觀察者,所以在應(yīng)用即將終止時(shí)和退到后臺(tái)時(shí)槽华,都會(huì)調(diào)用 -cleanDiskWithCompletionBlock: 方法來(lái)異步清掃緩存壹蔓,清掃磁盤緩存的邏輯是,先遍歷所有緩存文件猫态,并根據(jù)文件的修改時(shí)間來(lái)刪除過(guò)期的文件佣蓉,同時(shí)記錄剩下的文件的屬性和總體積大小,如果設(shè)置了 maxCacheAge 屬性的話亲雪,接下來(lái)就把剩下的文件按修改時(shí)間從小到大排序(最早的排最前面)勇凭,最后再遍歷這個(gè)文件數(shù)組,一個(gè)一個(gè)刪义辕,直到總體積小于 desiredCacheSize 為止虾标,也就是 maxCacheSize 的一半。

知識(shí)點(diǎn)

  1. NSCache 是什么灌砖?

    參考:
    NSCache Class Refernce
    Effective Objective-C 2.0(Item 50: Use NSCache Instead of NSDictionary for Caches)
    Foundation: NSCache
    NSCache 源碼(Swift)分析
    YYCache 設(shè)計(jì)思路

  2. 文件操作和 NSDirectoryEnumerator

  3. 如何判斷一個(gè)圖片的格式是 PNG 還是 JPG璧函?

3. 圖片加載管理器——SDWebImageManager

真正加載圖片時(shí),我們需要將下載和緩存兩個(gè)功能結(jié)合起來(lái)基显,這樣才算是一個(gè)完整的圖片加載器蘸吓,SDWebImageManager 就是專門干這個(gè)的。

幾個(gè)問(wèn)題

  • 讀取磁盤緩存操作和下載操作都是異步的撩幽,如何管理這兩個(gè)操作(operation)库继?
  • 對(duì)于下載失敗過(guò)的 URL,如何處理重試窜醉?

枚舉

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
};

.h 文件中的屬性:

@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache;              // 緩存器
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; // 下載器
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;      // 用來(lái)自定義緩存 key 的 block

.m 文件中的屬性:

@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
@property (strong, nonatomic) NSMutableSet *failedURLs;             // 下載失敗過(guò)的 URL 
@property (strong, nonatomic) NSMutableArray *runningOperations;    // 正在執(zhí)行中的任務(wù)

.h 文件中的方法:

+ (SDWebImageManager *)sharedManager;

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                        options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;


- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;

// Operation
- (void)cancelAll;
- (BOOL)isRunning;

// Check if image exists
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (BOOL)diskImageExistsForURL:(NSURL *)url;
- (void)cachedImageExistsForURL:(NSURL *)url completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (void)diskImageExistsForURL:(NSURL *)url completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;

- (NSString *)cacheKeyForURL:(NSURL *)url;

.m 文件中的方法:

// LifeCycle
+ (id)sharedManager;
- (id)init;
- (SDImageCache *)createCache;

// Cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;
 
// Check if image exists
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (BOOL)diskImageExistsForURL:(NSURL *)url;
- (void)cachedImageExistsForURL:(NSURL *)url completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (void)diskImageExistsForURL:(NSURL *)url completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;

// Load image
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                        options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

// Save image
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;

// Operation
- (void)cancelAll;
- (BOOL)isRunning;

具體實(shí)現(xiàn):

SDWebImageManager 的核心任務(wù)是由 -downloadImageWithURL:options:progress:completed: 方法來(lái)實(shí)現(xiàn)的宪萄,這個(gè)方法中先會(huì)從 SDImageCache 中讀取緩存,如果有緩存榨惰,就直接返回緩存雨膨,如果沒(méi)有就通過(guò) SDWebImageDownloader 去下載,下載成功后再保存到緩存中去读串,然后再回調(diào) completedBlock聊记。其中 progressBlock 的回調(diào)是直接交給了 SDWebImageDownloaderprogressBlock 來(lái)處理的。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    # 1. 對(duì) completedBlock 和 url 進(jìn)行檢查

    # 2. 創(chuàng)建 SDWebImageCombinedOperation 對(duì)象

    # 3. 判斷是否是曾經(jīng)下載失敗過(guò)的 url

    # 4. 如果這個(gè) url 曾經(jīng)下載失敗過(guò)恢暖,并且沒(méi)有設(shè)置 SDWebImageRetryFailed排监,就直回調(diào) completedBlock,并且直接返回

    # 5. 添加 operation 到 runningOperations 中
    
    # 6. 計(jì)算緩存用的 key杰捂,讀取緩存

    # 7. 處理緩存查詢結(jié)果回調(diào)

        # 7.1 判斷 operation 是否已經(jīng)被取消了舆床,如果已經(jīng)取消了就直接移除 operation
        # 7.2 進(jìn)一步處理
            # 7.2.A 如果緩存中沒(méi)有圖片或者圖片每次都需要更新
                # 7.2.A.1 如果有緩存圖片,先回調(diào) completedBlock,回傳緩存的圖片
                # 7.2.A.2 開始下載圖片挨队,獲得 subOperation
                    # 7.2.A.2.1.A 操作被取消谷暮,什么都不干
                    # 7.2.A.2.1.B 下載失敗
                        # 7.2.A.2.B.1 沒(méi)有被取消的話,回調(diào) completedBlock
                        # 7.2.A.2.B.2 如果需要盛垦,則將 URL 加入下載失敗的黑名單
                    # 7.2.A.2.1.C 下載成功
                        # 7.2.A.2.1.C.1 將 URL 從下載失敗的黑名單中移除
                        # 7.2.A.2.1.C.2 緩存圖片
                        # 7.2.A.2.1.C.3 回調(diào) completedBlock
                    # 7.2.A.2.2 將 operation 從 runningOperations 中移除
                # 7.2.1.3 設(shè)置 SDWebImageCombinedOperation 的 cancelBlock——cancel 掉 subOperation湿弦,并移除 operation
                
            # 7.2.B 如果有緩存圖片且不需要每次更新
                # 7.2.B.1 回調(diào) completedBlock
                # 7.2.B.2 流程結(jié)束,從 runningOperations 中移除 operation
            # 7.2.C 如果沒(méi)有緩存圖片而且不允許下載
                # 7.2.B.1 回調(diào) completedBlock
                # 7.2.B.2 流程結(jié)束腾夯,從 runningOperations 中移除 operation
}

SDWebImageManager 在讀取緩存和下載之前會(huì)創(chuàng)建一個(gè) SDWebImageCombinedOperation 對(duì)象颊埃,這個(gè)對(duì)象是用來(lái)管理緩存讀取操作和下載操作的,SDWebImageCombinedOperation` 對(duì)象有 3 個(gè)屬性:

  • cancelled:用來(lái)取消當(dāng)前加載任務(wù)的
  • cancelBlock:用來(lái)移除當(dāng)前加載任務(wù)和取消下載任務(wù)的
  • cacheOperation:用來(lái)取消讀取緩存操作

知識(shí)點(diǎn)

4. 設(shè)置 UIImageView 的圖片——UIImageView+WebCache

我們平時(shí)最常用的圖片加載蝶俱,是通過(guò)調(diào)用 UIImageView+WebCache-sd_setImageWithURL:... 系列方法來(lái)加載的班利,UIImageView+WebCache 實(shí)際上是將 SDWebImageManager 封裝了一層,內(nèi)部針對(duì) UIImageView 做了一些處理榨呆,使用起來(lái)更方便罗标、更直接、更簡(jiǎn)單积蜻。

UIImageView+WebCache 的主要任務(wù)是以下幾個(gè):

  • 占位圖設(shè)置
  • 自動(dòng)管理圖片加載任務(wù)
  • 圖片成功獲取后闯割,自動(dòng)設(shè)置圖片顯示

幾個(gè)問(wèn)題

  • 如何處理 UIImageView 連續(xù)多次加載圖片的情況,比如在 UITableView...cellForRow... 方法中加載 cell 上的圖片浅侨?
  • 如何處理 placeholder image 的顯示邏輯纽谒?

.h 文件中的方法:

- (NSURL *)sd_imageURL;

// Load image for UIImageView
- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url andPlaceholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

// Animation Image
- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;

// Cancel
- (void)sd_cancelCurrentImageLoad;
- (void)sd_cancelCurrentAnimationImagesLoad;

.m 文件中的方法:

同 .h 文件中的方法

具體實(shí)現(xiàn):

UIImageView+WebCache 的核心邏輯都在 - sd_setImageWithURL:placeholderImage:options:progress:completed: 方法中证膨,為了防止多個(gè)異步加載任務(wù)同時(shí)存在時(shí)如输,可能出現(xiàn)互相沖突和干擾,該方法中首先通過(guò)調(diào)用 -sd_cancelCurrentImageLoad 方法取消這個(gè) UIImageView 當(dāng)前的下載任務(wù)央勒,然后設(shè)置了占位圖不见,如果 url 不為 nil,接著就調(diào)用 SDWebImageManager-downloadImage... 方法開始加載圖片崔步,并將這個(gè)加載任務(wù) operation 保存起來(lái)稳吮,用于后面的 cancel 操作。圖片獲取成功后井濒,再重新設(shè)置 imageView 的 image灶似,并回調(diào) completedBlock。

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    # 1. 取消當(dāng)前正在進(jìn)行的加載任務(wù)
    # 2. 通過(guò) Associated Object 將 url 作為成員變量存起來(lái)

    # 3. 設(shè)置占位圖

    # 4. 根據(jù) url 是否為 nil 做處理
        A. 如果 url 不為 nil
            A.1 調(diào)用 SDWebImageManager 的 -downloadImage... 方法開始加載圖片瑞你,并獲得一個(gè) operation
                A.1.1 設(shè)置 image
                    A.1.1.A 圖片下載成功酪惭,設(shè)置 image
                    A.1.1.B 圖片下載失敗,設(shè)置 placeholder
                    A.1.1.C 如果不需要自動(dòng)設(shè)置 image者甲,直接 return
                A.1.2 回調(diào) completedBlock
            A.2 借助 UIView+WebCacheOperation 將獲得的 operation 保存到成員變量中去
        B. URL 為空時(shí)春感,直接回調(diào) completedBlock,返回錯(cuò)誤信息
}

值得注意的是,為了防止多個(gè)異步加載任務(wù)同時(shí)存在時(shí)鲫懒,可能出現(xiàn)互相沖突和干擾嫩实,每個(gè) UIImageView 的圖片加載任務(wù)都會(huì)保存成一個(gè) Associated Object,方便需要時(shí)取消任務(wù)窥岩。這個(gè) Associated Object 的操作是在 UIView+WebCacheOperation 中實(shí)現(xiàn)的甲献,因?yàn)槌?UIImageView 用到圖片加載功能之外,還有 UIButton 等其他類也用到了加載遠(yuǎn)程圖片的功能谦秧,所以需要進(jìn)行同樣的處理竟纳,這樣設(shè)計(jì)實(shí)現(xiàn)了代碼的復(fù)用。

知識(shí)點(diǎn)

  1. UI 操作為什么必須在主線程執(zhí)行疚鲤?
  2. -setNeedsLayout 方法

四锥累、知識(shí)點(diǎn)概覽

1.TARGET_OS_IPHONE 宏和 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 宏的使用
這兩個(gè)宏都是用于編譯時(shí)進(jìn)行 SDK 版本適配的宏,主要用于模擬器上的調(diào)試集歇,而針對(duì)真機(jī)上的 iOS 版本適配就需要采用運(yùn)行時(shí)的判斷方式了桶略,比如使用 respondsToSelector: 方法來(lái)判斷當(dāng)前運(yùn)行環(huán)境是否支持該方法的調(diào)用。

參考:http://stackoverflow.com/questions/3269344/what-is-difference-between-these-2-macros/3269562#3269562
http://stackoverflow.com/questions/7542480/what-are-the-common-use-cases-for-iphone-os-version-max-allowed

2.typeof__typeof诲宇,__typeof__ 的區(qū)別

參考:http://stackoverflow.com/questions/14877415/difference-between-typeof-typeof-and-typeof-objective-c

3.使用 -[UIApplication beginBackgroundTaskWithExpirationHandler:] 方法使 app 退到后臺(tái)時(shí)還能繼續(xù)執(zhí)行任務(wù), 不再執(zhí)行后臺(tái)任務(wù)時(shí)际歼,需要調(diào)用 -[UIApplication endBackgroundTask:] 方法標(biāo)記后臺(tái)任務(wù)結(jié)束。
參考:https://developer.apple.com/reference/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio
objective c - Proper use of beginBackgroundTaskWithExpirationHandler
iOS Tips and Tricks: Working in the Background
Background Modes Tutorial: Getting Started

  1. NSFoundationVersionNumber 的使用

    參考:http://stackoverflow.com/questions/19990900/nsfoundationversionnumber-and-ios-versions

  1. SDWebImage 文檔中的兩張 Architecture 圖怎么看姑蓝?什么是 UML 類圖鹅心?

6.SDWebImage 的緩存路徑?
格式:Libray/Cache/<#namespace#>/com.hackemist.SDWebImageCache.<#namespace#>/<#MD5_filename#>
如果是默認(rèn)的 namespace纺荧,那么路徑就是 Library/cache/default/com.hackemist.SDWebImageCache.default/<#MD5_filename#>旭愧,詳見(jiàn) -storeImage:recalculateFromImage:imageData:forKey:toDisk 方法和 -defaultDiskCachePath 方法

7.文件的緩存有效期及最大緩存空間大小

- 默認(rèn)有效期:```maxCacheAge = 60 * 60 * 24 * 7; // 1 week```
- 默認(rèn)最大緩存空間:```maxCacheSize = <#unlimited#>```

8.MKAnnotationView 是用來(lái)干嘛的?
MKAnnotationView 是屬于 MapKit 框架的一個(gè)類宙暇,繼承自 UIView输枯,是用來(lái)展示地圖上的 annotation 信息的,它有一個(gè)用來(lái)設(shè)置圖片的屬性 image 占贫。
See API Reference: MKAnnotationView

9.圖片下載完成后桃熄,為什么需要用 SDWebImageDecoder 進(jìn)行解碼?

10.SDWebImage 中圖片緩存的 key 是按照什么規(guī)則取的型奥?

  1. SDImageCache 清除磁盤緩存的過(guò)程瞳收?

  2. md5 是什么算法?是用來(lái)干什么的厢汹?除此之外螟深,還有哪些類似的加密算法?

  1. SDImageCache 讀取磁盤緩存是不是就是指從沙盒中查找并讀取文件坑匠?

五血崭、收獲與疑問(wèn)

  1. UIImageView 是如何通過(guò) SDWebImage 加載圖片的?
  2. SDWebImage 在設(shè)計(jì)上有哪些巧妙之處?
  3. 假如我自己來(lái)實(shí)現(xiàn)一個(gè)圖片下載工具夹纫,我該怎么寫咽瓷?
  4. SDWebImage 的進(jìn)化史
  5. SDWebImage 的性能怎么看?
  6. SDWebImage 是如何處理 gif 圖的舰讹?

六茅姜、啟發(fā)與實(shí)踐

在閱讀 SDWebImage 源碼的過(guò)程中,受到了不少啟發(fā)月匣,所以在不斷完善生成快照功能這個(gè)需求時(shí)钻洒,做了不少重構(gòu)工作,思路也是越做越清晰锄开。

七素标、延伸閱讀


Contact Me
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末萍悴,一起剝皮案震驚了整個(gè)濱河市头遭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌癣诱,老刑警劉巖救军,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涩笤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡朋譬,警方通過(guò)查閱死者的電腦和手機(jī)拆吆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門琼懊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)抡笼,“玉大人锚贱,你說(shuō)我怎么就攤上這事±绞酰” “怎么了艺蝴?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵猬腰,是天一觀的道長(zhǎng)鸟废。 經(jīng)常有香客問(wèn)我,道長(zhǎng)姑荷,這世上最難降的妖魔是什么盒延? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鼠冕,結(jié)果婚禮上添寺,老公的妹妹穿的比我還像新娘。我一直安慰自己懈费,他們只是感情好计露,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般票罐。 火紅的嫁衣襯著肌膚如雪叉趣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天该押,我揣著相機(jī)與錄音疗杉,去河邊找鬼。 笑死蚕礼,一個(gè)胖子當(dāng)著我的面吹牛烟具,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奠蹬,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼朝聋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了囤躁?” 一聲冷哼從身側(cè)響起玖翅,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎割以,沒(méi)想到半個(gè)月后金度,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡严沥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年猜极,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片消玄。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跟伏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翩瓜,到底是詐尸還是另有隱情受扳,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布兔跌,位于F島的核電站勘高,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏坟桅。R本人自食惡果不足惜华望,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仅乓。 院中可真熱鬧赖舟,春花似錦、人聲如沸夸楣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至石洗,卻和暖如春痛单,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劲腿。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工旭绒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人焦人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓挥吵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親花椭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忽匈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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