1. SDWebImage內(nèi)部實(shí)現(xiàn)原理步驟
1.1 實(shí)現(xiàn)步驟
1. 入口setImageWithUrl:placeHolderImage:options:會(huì)把placeHolderImage顯示,然后SDWebImageManager根據(jù)URL開始處理圖片.
2. 進(jìn)入SDWebImageManager-downloadWithURL:delegate:options:userInfo:交給SDImageCache從緩存查找圖片是否已經(jīng)下載queryDiskCacheForKey:delegate:userInfo:
3. 先從內(nèi)存圖片緩存查找是否有圖片,如果內(nèi)存中已經(jīng)有圖片緩存,SDImageCacheDelegate回調(diào)imageCache:didFineImage:forKey:userInfo:到SDWebImageManager.
4. SDWebImageManagerDelegate回調(diào)webImageManager:didFinishWithImage:到UIImageView + WebCache等前端展示圖片.
5. 如果內(nèi)存緩存中沒有,生成NSInvocationOperation添加到隊(duì)列開始從硬盤查找圖片是否已經(jīng)緩存
6. 根據(jù)URLKey在硬盤緩存目錄下嘗試讀取圖片文件.這一步是在NSOperation進(jìn)行的操作,所以回主線程進(jìn)行結(jié)果回調(diào)notifyDelegate.
7. 如果上一操作從硬盤讀取到了圖片,將圖片添加到內(nèi)存緩存中(如果空閑內(nèi)存過小 會(huì)先清空內(nèi)存緩存).SDImageCacheDelegate 回調(diào)imageCache:didFinishImage:forKey:userInfo:進(jìn)而回調(diào)展示圖片.
8. 如果從硬盤緩存目錄讀取不到圖片,說明所有緩存都不存在該圖片,需要下載圖片,回調(diào)imageCache:didNotFindImageForKey:userInfo.
9. 共享或重新生成一個(gè)下載器SDWebImageDownLoader開始下載圖片
10. 圖片下載由NSURLConnection來做,實(shí)現(xiàn)相關(guān)delegate來判斷圖片下載中,下載完成和下載失敗
11. connection:didReceiveData:中利用ImageIO做了按圖片下載進(jìn)度加載效果
12. connectionDidFinishLoading:數(shù)據(jù)下載完成后交給SDWebImageDecoder做圖片解碼處理
13. 圖片解碼處理在一個(gè)NSOperationQueue完成,不會(huì)拖慢主線程UI.如果有需要對(duì)下載的圖片進(jìn)行二次處理,最好也在這里完成,效率會(huì)好很多.
14. 在主線程notifyDelegateOnMainThreadWithInfo:宣告解碼完成imageDecoder:didFinishDecodingImage:userInfo:回調(diào)給SDWebImageDownloader
15. imageDownLoader:didFinishWithImage:回調(diào)給SDWebImageManager告知圖片下載完成
16. 通知所有的downloadDelegates下載完成,回調(diào)給需要的地方展示圖片
17. 將圖片保存到SDImageCache中內(nèi)存緩存和硬盤緩存同時(shí)保存,寫文件到硬盤也在以單獨(dú)NSInvocationOperation完成,避免拖慢主線程
18. SDImageCache在初始化的時(shí)候會(huì)注冊(cè)一些消息通知,在內(nèi)存警告或退到后臺(tái)的時(shí)候清理內(nèi)存圖片緩存,應(yīng)用結(jié)束的時(shí)候清理過期圖片
19. SDWI也提供UIButton + WebCache和MKAnnptation + WebCache方便使用
20. SDWebImagePrefetcher 可以預(yù)先下載圖片,方便后續(xù)使用
再用一張圖說明:

1.2 API中參數(shù)枚舉類型
1.2.1 SDWebImageOptions:圖片下載策略
例如,SD為UIImageView提供的UIImageView+WebCache.m分類,有這些API:
- (void)sd_setImageWithURL:(NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}
其實(shí),以上這些API都直接或間接利用到了SDWebImageOptions這個(gè)參數(shù),那么你記得這個(gè)參數(shù)有哪些類型?各有什么作用?
通過查看SDWebImageManager.h源代碼,可知如下:
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
// 默認(rèn)情況下,當(dāng)URL下載失敗時(shí),URL會(huì)被列入黑名單,導(dǎo)致庫(kù)不會(huì)再去重試,該標(biāo)記用于禁用黑名單
SDWebImageRetryFailed = 1 << 0,
// 默認(rèn)情況下,圖片下載開始于UI交互,該標(biāo)記禁用這一特性,這樣下載延遲到UIScrollView減速時(shí)
SDWebImageLowPriority = 1 << 1,
// 該標(biāo)記禁用磁盤緩存
SDWebImageCacheMemoryOnly = 1 << 2,
// 該標(biāo)記啟用漸進(jìn)式下載,圖片在下載過程中是漸漸顯示的,如同瀏覽器一下。
// 默認(rèn)情況下,圖像在下載完成后一次性顯示
SDWebImageProgressiveDownload = 1 << 3,
// 即使圖片緩存了,也期望HTTP響應(yīng)cache control,并在需要的情況下從遠(yuǎn)程刷新圖片。
// 磁盤緩存將被NSURLCache處理而不是SDWebImage,因?yàn)镾DWebImage會(huì)導(dǎo)致輕微的性能下載。
// 該標(biāo)記幫助處理在相同請(qǐng)求URL后面改變的圖片。如果緩存圖片被刷新,則完成block會(huì)使用緩存圖片調(diào)用一次
// 然后再用最終圖片調(diào)用一次
SDWebImageRefreshCached = 1 << 4,
// 在iOS 4+系統(tǒng)中,當(dāng)程序進(jìn)入后臺(tái)后繼續(xù)下載圖片。這將要求系統(tǒng)給予額外的時(shí)間讓請(qǐng)求完成
// 如果后臺(tái)任務(wù)超時(shí),則操作被取消
SDWebImageContinueInBackground = 1 << 5,
// 通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES;來處理存儲(chǔ)在NSHTTPCookieStore中的cookie
SDWebImageHandleCookies = 1 << 6,
// 允許不受信任的SSL認(rèn)證
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
// 默認(rèn)情況下,圖片下載按入隊(duì)的順序來執(zhí)行。該標(biāo)記將其移到隊(duì)列的前面,
// 以便圖片能立即下載而不是等到當(dāng)前隊(duì)列被加載
SDWebImageHighPriority = 1 << 8,
// 默認(rèn)情況下,占位圖片在加載圖片的同時(shí)被加載。該標(biāo)記延遲占位圖片的加載直到圖片已以被加載完成
SDWebImageDelayPlaceholder = 1 << 9,
// 通常我們不調(diào)用動(dòng)畫圖片的transformDownloadedImage代理方法,因?yàn)榇蠖鄶?shù)轉(zhuǎn)換代碼可以管理它。
// 使用這個(gè)票房則不任何情況下都進(jìn)行轉(zhuǎn)換。
SDWebImageTransformAnimatedImage = 1 << 10,
};
1.2.2 SDImageCacheType:圖片緩存策略
例如,設(shè)置圖片的兩個(gè)例子
[self.image2 sd_setImageWithURL:imagePath2 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
NSLog(@"這里可以在圖片加載完成之后做些事情");
}];
//使用默認(rèn)圖片,而且用block 在完成后做一些事情
[self.image1 sd_setImageWithURL:imagePath1 placeholderImage:[UIImage imageNamed:@"default"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
NSLog(@"圖片加載完成后做的事情");
}];
還有獲取下載進(jìn)度的例子
//使用默認(rèn)圖片,而且用block 在完成后做一些事情
[self.image1 sd_setImageWithURL:imagePath1 placeholderImage:[UIImage imageNamed:@"default"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
NSLog(@"圖片加載完成后做的事情");
}];
上面的例子,都有個(gè)SDImageCacheTyp的參數(shù),你記得這個(gè)參數(shù)有哪些?
- SDImageCacheType
//定義Cache類型
typedef NS_ENUM(NSInteger, SDImageCacheType) {
//不使用cache獲得圖片,依然會(huì)從web下載圖片
SDImageCacheTypeNone,
//圖片從disk獲得
SDImageCacheTypeDisk,
//圖片從Memory中獲得
SDImageCacheTypeMemory
};
2. 最大緩存和時(shí)間設(shè)置
- SDImageCache類的源碼
//這個(gè)變量默認(rèn)值為YES,顯示比較高質(zhì)量的圖片,但是會(huì)浪費(fèi)比較多的內(nèi)存,可以通過設(shè)置NO來緩解內(nèi)存
@property (assign, nonatomic) BOOL shouldDecompressImages;
//總共的內(nèi)存允許圖片的消耗值
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//圖片存活于內(nèi)存的時(shí)間初始化的時(shí)候默認(rèn)為一周
@property (assign, nonatomic) NSInteger maxCacheAge;
//每次存儲(chǔ)圖片大小的限制
@property (assign, nonatomic) NSUInteger maxCacheSize;
- 設(shè)置maxCacheSize的例子
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager.imageCache setMaxMemoryCost:1000000];//設(shè)置總緩存大小,默認(rèn)為0沒有限制
[manager.imageCache setMaxCacheSize:640000];//設(shè)置單個(gè)圖片限制大小
[manager.imageDownloader setMaxConcurrentDownloads:1];//設(shè)置同時(shí)下載線程數(shù),這是下載器的內(nèi)容,下面將會(huì)介紹
[manager downloadImageWithURL:[NSURL URLWithString:@"http://p9.qhimg.com/t01eb74a44c2eb43193.jpg"]
options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {
NSLog(@"%lu", receivedSize);
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
self.imageView1.image = image;
}];
[manager downloadImageWithURL:[NSURL URLWithString:@"http://img.article.pchome.net/00/28/33/87/pic_lib/wm/kuanpin12.jpg"]
options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {
NSLog(@"%lu", receivedSize);
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
self.imageView2.image = image;
}];
NSUInteger size = [manager.imageCache getSize];
NSUInteger count = [manager.imageCache getDiskCount];
NSLog(@"size = %lu", size); // 644621(兩張測(cè)試圖片)
NSLog(@"count = %lu", count); // 2
[manager.imageCache clearDisk];
size = [manager.imageCache getSize];
count = [manager.imageCache getDiskCount];
NSLog(@"sizeClean = %lu", size); // 0
NSLog(@"countClean = %lu", count); // 0 這里使用的是clear
3. 區(qū)分:三種種緩存(內(nèi)存圖片緩存,磁盤圖片緩存,內(nèi)存操作緩存)
- 先查看內(nèi)存圖片緩存,內(nèi)存圖片緩存沒有,后生成操作,查看磁盤圖片緩存
- 磁盤圖片緩存有,就加載到內(nèi)存緩存,沒有就下載圖片
- 在建立下載操作之前,判斷下載操作是否存在
- 默認(rèn)情況下,下載的圖片數(shù)據(jù)會(huì)同時(shí)緩存到內(nèi)存和磁盤中

關(guān)于緩存位置
- 內(nèi)存緩存是通過 NSCache的子類AutoPurgeCache來實(shí)現(xiàn)的;
- 磁盤緩存是通過 NSFileManager 來實(shí)現(xiàn)文件的存儲(chǔ)(默認(rèn)路徑為/Library/Caches/default/com.hackemist.SDWebImageCache.default),是異步實(shí)現(xiàn)的。
關(guān)于圖片下載操作
SDWebImage的大部分工作是由緩存對(duì)象SDImageCache和異步下載器管理對(duì)象SDWebImageManager來完成的。
SDWebImage的圖片下載是由SDWebImageDownloader這個(gè)類來實(shí)現(xiàn)的,它是一個(gè)異步下載管理器,下載過程中增加了對(duì)圖片加載做了優(yōu)化的處理。而真正實(shí)現(xiàn)圖片下載的是自定義的一個(gè)Operation操作,將該操作加入到下載管理器的操作隊(duì)列downloadQueue中,Operation操作依賴系統(tǒng)提供的NSURLConnection類實(shí)現(xiàn)圖片的下載。
4. 高清和低清圖片與網(wǎng)絡(luò)環(huán)境的問題
- 網(wǎng)絡(luò)判斷的問題--利用AFNetworking的API
首先,啟用監(jiān)控
// AppDelegate.m 文件中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// 監(jiān)控網(wǎng)絡(luò)狀態(tài)
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
}
然后,在需要的地方獲取監(jiān)控管理
// 以下代碼在需要監(jiān)聽網(wǎng)絡(luò)狀態(tài)的方法中使用
AFNetworkReachabilityManager *mgr = [AFNetworkReachabilityManager sharedManager];
if (mgr.isReachableViaWiFi) { // 在使用Wifi, 下載原圖
} else { // 其他,下載小圖
}
并不能簡(jiǎn)單的這樣:WIFI就下載高清圖,蜂窩網(wǎng)絡(luò)就下載縮略圖。要考慮和利用緩存的因素。
一個(gè)典型例子
- setItem:(CustomItem *)item
{
_item = item;
// 占位圖片
UIImage *placeholder = [UIImage imageNamed:@"placeholderImage"];
// 從內(nèi)存\沙盒緩存中獲得原圖,
UIImage *originalImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:item.originalImage];
if (originalImage) { // 如果內(nèi)存\沙盒緩存有原圖,那么就直接顯示原圖(不管現(xiàn)在是什么網(wǎng)絡(luò)狀態(tài))
self.imageView.image = originalImage;
} else { // 內(nèi)存\沙盒緩存沒有原圖
AFNetworkReachabilityManager *mgr = [AFNetworkReachabilityManager sharedManager];
if (mgr.isReachableViaWiFi) { // 在使用Wifi, 下載原圖
[self.imageView sd_setImageWithURL:[NSURL URLWithString:item.originalImage] placeholderImage:placeholder];
} else if (mgr.isReachableViaWWAN) { // 在使用手機(jī)自帶網(wǎng)絡(luò)
// 用戶的配置項(xiàng)假設(shè)利用NSUserDefaults存儲(chǔ)到了沙盒中
// [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"alwaysDownloadOriginalImage"];
// [[NSUserDefaults standardUserDefaults] synchronize];
#warning 從沙盒中讀取用戶的配置項(xiàng):在3G\4G環(huán)境是否仍然下載原圖
BOOL alwaysDownloadOriginalImage = [[NSUserDefaults standardUserDefaults] boolForKey:@"alwaysDownloadOriginalImage"];
if (alwaysDownloadOriginalImage) { // 下載原圖
[self.imageView sd_setImageWithURL:[NSURL URLWithString:item.originalImage] placeholderImage:placeholder];
} else { // 下載小圖
[self.imageView sd_setImageWithURL:[NSURL URLWithString:item.thumbnailImage] placeholderImage:placeholder];
}
} else { // 沒有網(wǎng)絡(luò)
UIImage *thumbnailImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:item.thumbnailImage];
if (thumbnailImage) { // 內(nèi)存\沙盒緩存中有小圖
self.imageView.image = thumbnailImage;
} else { // 處理離線狀態(tài),而且有沒有緩存時(shí)的情況
self.imageView.image = placeholder;
}
}
}
}
5. 可能的問題
5.1 后臺(tái)沒有處理的高清大圖導(dǎo)致APP內(nèi)存過大而奔潰?
- 思路,改寫sd_imageWithData方法的源代碼,可參考https://blog.csdn.net/benyoulai5/article/details/50462586
5.2 SD怎樣解決Cell移出屏幕后的cell中的UIImageView繼續(xù)下載問題?-- 移除UIImageView當(dāng)前綁定的操作。
- SD為設(shè)置UIImageView提供的API,歸根結(jié)底調(diào)用的是下面API:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
- 它的方法實(shí)現(xiàn)體中,第一句話就是
[self sd_cancelCurrentImageLoad]; - 這個(gè)非常關(guān)鍵,當(dāng)在TableView的cell包含了的UIImageView被重用時(shí),首先調(diào)用這一行代碼,保證這個(gè)ImageView的下載和緩存組合操作都被取消。如果:
①上次賦值的圖片正在下載,則下載不再進(jìn)行;
②下載完成了,但還沒有執(zhí)行到調(diào)用回調(diào)(回調(diào)包含wself.image = image),由于操作被取消,因而不會(huì)顯示和重用的cell相同的圖片;
③以上兩種情況只有在網(wǎng)速極慢和手機(jī)處理速度極慢的情況下才會(huì)發(fā)生,實(shí)際上發(fā)生的概率非常小,大多數(shù)是這種情況:操作已經(jīng)進(jìn)行到下載完成了,這次使用的cell是一個(gè)重用的cell,而且保留著imageView的image,對(duì)于這種情況SD會(huì)在該實(shí)現(xiàn)方法里面接著設(shè)置占位圖的語句,將image暫時(shí)設(shè)置為占位圖,如果占位圖為空,就意味著先暫時(shí)清空image。

- SD內(nèi)部移除綁定的操作的調(diào)用棧為:
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
5.3 SDWebImage怎么實(shí)現(xiàn)屏幕滑動(dòng)時(shí),暫停數(shù)據(jù)源的任務(wù)?通過NSOperationQueue的setSuspend嗎?
3.1 基于NSURLConnection的SDWebImage
(至少2014年7月的版本)老版本的基于 NSURLConnection 的 SDWebImage 是通過這樣的機(jī)制:NSURLConnection工作在主線程,雖然NSURLConnection工作在子線程,但因?yàn)閁I相關(guān)的操作和回調(diào)中的setImage都在同一個(gè)主線程,滑動(dòng)屏幕會(huì)導(dǎo)致主線程的runloop切換mode為UITrackingRunLoopMode,此時(shí)原來kCFRunLoopDefaultMode上的source (這里是NSURLConnectionsetImage) 都被暫停,runloop執(zhí)行不到回調(diào)。
它的本意是不讓網(wǎng)絡(luò)相關(guān)的操作阻塞到主線程,改正:網(wǎng)絡(luò)相關(guān)的操作在子線程,主線程runloop的mode切換并不會(huì)影響子線程,但是它這樣設(shè)計(jì)的確有這樣的效果:屏幕滑動(dòng)時(shí),暫停數(shù)據(jù)下載的任務(wù),改正:滑動(dòng)屏幕并不會(huì)暫停數(shù)據(jù)下載,暫停的是同一個(gè)主線程的setImage。
在老版本中,在SDWebImageDownloaderOperation.m文件中有這樣一段話:

3.2 基于NSURLSession的SDWebImage
然而,新版本的 SDWebImage 是基于 NSURLSession 的,這個(gè)NSURLSession不同于NSURLConnection的最大區(qū)別是不是基于主線程 子線程 的runloop控制的,而是通過NSOperation新開子線程,所以同意主線程的runloop切換mode并不會(huì)影響子線程的操作。所以,新版本的SDWebImage是沒有這個(gè)“滑動(dòng)即暫?!钡男Ч?/del>。改正:同樣,滑動(dòng)屏幕并不會(huì)暫停數(shù)據(jù)下載,暫停的是同一個(gè)主線程的setImage。
如果,實(shí)在有需要,有兩種辦法,可以自己改寫setImage的方法,在里面設(shè)置工作的mode,同老版的SDWebImage一樣改正:一種是改變setImage的線程或者mode。還有一種辦法,可以監(jiān)聽ScrollView的拉拽狀態(tài),當(dāng)ScrollView的代理方法監(jiān)聽到被拉拽,就suspend操作。