讀完這篇文章你可以自己寫一個 輕量級別的SDWebImage神器,這篇文章類似源碼解析。但不同的是,不僅僅是解析,會帶你手把手擼一個精簡版的SDWebImage,更深刻的理解SDWebImage的架構(gòu)和一些核心類的功能。學習一個東西必須要總結(jié)一次才能更加理解其中的精華,否則即便是讀完了源碼也學不到多少核心的動心。好記性不如爛筆頭。
注:SDWebImage有很多功能,我這里就實現(xiàn)了一個核心的功能,多圖異步下載、內(nèi)存緩存、磁盤緩存。
輕量級仿SDWebImage源碼下載鏈接:https://github.com/ZhengYaWei1992/ZWWebImageCache
就直接從功能實現(xiàn)開始,仿SDWebImage架構(gòu)手把手擼一個輕量級SDWebImage,之后在針對SDWebImage框架深入分析,因為前期的實現(xiàn)都是模仿SDWebImage的實現(xiàn),所以當你看懂前面的部分后,再去理解SDWebImage就是太輕而易舉的事了。
一、輕量級SDWebImage的實現(xiàn)
1.1 、仿SDWebImage的架構(gòu)設計思路
先總的看一下架構(gòu)設計思路,如下圖:

這個圖可以先從Controller看起,Controller主要是否則調(diào)用UIImageView扥類中的方法,相信大家都知道SDWebImage最基本的設置圖像的方法,需要導入UIImageView+webCache這個分類,這個架構(gòu)同樣是采用這種方法,通過UIImageView的分類設置圖像,只用簡單的傳入一個urlString即可,當然占位圖是必然支持的。
下載操作類:
UIImageView分類中包含下載操作管理類,下載操作管理類被設計為一個單例對象,它是這個架構(gòu)中的核心,緩存、下載操作都是由它同意進行管理。因為緩存類本身涉及內(nèi)容不是很多,所以這里就沒有給緩存類單獨抽離開來。
下載操作類:
首先要說明一下,圖片的異步下載,這里主要是通過NSOperationQueue這個類實現(xiàn)的。所謂的下載類就是NSOperation,每一個操作都對應一個實例對象。下載操作類的實現(xiàn),這里主要是自定義一個類繼承自NSOperation,自定義下載操作。 它同樣是由下載操作管理類進行管理。
#######關于緩存:
SDWebImage的緩存形式實際上是包含內(nèi)存緩存和磁盤緩存。其中內(nèi)存緩存主要是借助NSCache這個類實現(xiàn)的,磁盤緩存就是常規(guī)的文件讀取啦。同樣這個輕量級的SDWebImage內(nèi)存緩存也是借助NSCache這個類實現(xiàn)的。關于NSCache這個類,可能日常開發(fā)中用的不是很普遍,但使用起來還是很簡單的,基本使用形式和字典類似,但是又有很多和字典不同之處。這篇文章中會說一些NSCache的使用和注意事項。
1.2 下載操作類的實現(xiàn)
下載操作類實際是一個繼承與 NSOperation的自定義類,該類中主要有兩個核心方法:初始化對象和系統(tǒng)方法main。說明:main方法是系統(tǒng)方法,在操作添加到隊列的時候會調(diào)用此方法。對外提供了兩個屬性圖片的urlString以及下載完成的回調(diào)(主要是在main方法中實現(xiàn))。
操作初始化方法
+ (instancetype)downloaderOperationWithURLString:(NSString *)urlString finishedBlock:(void (^)(UIImage *image))finishedBlock{
ZWDownloadOperation *op = [[ZWDownloadOperation alloc]init];
op.urlString = urlString;
op.finishedBlock = finishedBlock;
return op;
}
重寫系統(tǒng)main方法,當外部將該類的實例對象添加到隊列中時,會調(diào)用mian方法。main方法中之所以會出現(xiàn)自動autoreleasepool,主要是因為異步操作無法訪問主線程的自動釋放池,所以要手動自己添加釋放池。調(diào)用此方法會將讀取的圖片緩到磁盤中,下載完成后,會回到主線程產(chǎn)生回調(diào),并返回UIImage對象。
//重寫main方法 操作添加到隊列的時候會調(diào)用該方法
- (void)main{
//創(chuàng)建自動釋放池:因如果是異步操作,無法訪問主線程的自動釋放池
@autoreleasepool {
//斷言
//添加斷言后,if (self.finishedBlock) 不用再設置,如果為空了,程序會崩潰,同時會提醒:finishedBlock不能為空
NSAssert(self.finishedBlock != nil, @"finishedBlock不能為空");
//下載網(wǎng)絡圖片
NSURL *url = [NSURL URLWithString:self.urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
//緩存到沙盒中
if (data) {
[data writeToFile:[self.urlString appendCacheDir] atomically:YES];
}
//這里是子線程
//NSLog(@"下載圖片 %@ %@",self.urlString,[NSThread currentThread]);
NSLog(@"從網(wǎng)絡下載圖片");
//判斷?操作是否被取消
//如果取消,直接return。放在耗時操作之后和合理一些,取消操作的時候,不會攔截耗時操作,耗時操作依然可以執(zhí)行。下次想顯示圖像的時候,耗時操作也執(zhí)行完畢
if (self.isCancelled) {
return;
}
//圖片下載完成回到主線程更新UI 通過使用斷言,這里就不用使用if (self.finishedBlock) 判斷了
//if (self.finishedBlock) {
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
UIImage *img = [UIImage imageWithData:data];
self.finishedBlock(img);
}];
//}
}
}
1.3 下載操作管理類
毫無疑問,這個類必然是單例對象,管理類嗎,當然要全局管理,有足夠高的權(quán)限才能夠被稱為管理者。這個類主要是對完提供了三個方法:1、創(chuàng)建單例對象 2、開啟下載任務 3、取消操作,因為要考慮到重復下載的情況,所以要對外提供這樣一個接口。
說是下載操作管理類,實際上是有點不合適的,應為該類主要用有兩個功能:管理全局下載和管理全局緩存。全局緩存沒有單獨抽離出來,暫時就稱為下載操作管理類就行,緩存會單獨講解一些的。實際SDWebImage的緩存功能室單獨抽取出來的。
下載操作管理類主要提供了這樣三個屬性。分別是全局隊列、下載操作緩存池、圖片內(nèi)存緩存池。之所以會有下載操作緩存池,是因為要記錄下載操作,如果下載操作已經(jīng)存在就不用再去執(zhí)行下載方法,直接reture,避免重復下載這種情況的出現(xiàn)。開始下載圖片的時候,將草案做添加到操作緩存翅中。圖片下載完成后,操作要從操作緩存池中移除。
//全局隊列
@property(nonatomic,strong)NSOperationQueue *queue;
//下載操作緩存池 這里不能改為NSCache,因為收到內(nèi)存警告后NSCache移除所有對象,之后NSCache中就無法繼續(xù)添加數(shù)據(jù)了
@property(nonatomic,strong)NSMutableDictionary *operationCache;
//圖片緩存池(內(nèi)存緩存) 從字典改為NSCache
@property(nonatomic,strong)NSCache *imageCache;
下載方法的實現(xiàn)。
總的思路是這樣的,先判斷下載操作是否存在,如存在直接返回,避免重復下載。之后根據(jù)圖片地址urlString判斷是否存在內(nèi)存緩存和磁盤緩存,如果存在直接調(diào)用回調(diào),如過不存在就創(chuàng)建操作對象,添加到全局隊列,開啟下載任務。
- (void)downloadWithURLString:(NSString *)urlString finishedBlock:(void (^)(UIImage *image))finishedBlock{
//斷言
NSAssert(finishedBlock != nil, @"finishedBlock不能為空");
//如果下載操作已經(jīng)存在,直接返回。避免重復下載
if (self.operationCache[urlString]) {
return;
}
//判斷圖片是否有緩存(內(nèi)存和磁盤緩存)
if ([self checkImageCache:urlString]) {
//如果有緩存,就要回調(diào)設置圖像
finishedBlock([self.imageCache objectForKey:urlString]);
return;
}
ZWDownloadOperation *op = [ZWDownloadOperation downloaderOperationWithURLString:urlString finishedBlock:^(UIImage *image) {
//回調(diào)
finishedBlock(image);
//緩存圖片(內(nèi)存緩存)
//self.imageCache[urlString] = image;
[self.imageCache setObject:image forKey:urlString];
//下載完成,移除緩存的操作
[self.operationCache removeObjectForKey:urlString];
}];
[self.queue addOperation:op];
//緩存下載操作
self.operationCache[urlString] = op;
}
關于取消操作。
取消操作中藥判斷urlString是否為空,如果不做此判斷,當urlString為nil的時候,執(zhí)行 [self.operationCache removeObjectForKey:urlString];會發(fā)生崩潰。
//取消操作
- (void)cancelOperation:(NSString *)urlString{
//避免第一次urlString為空,然后調(diào)用[self.operationCache removeObjectForKey:urlString]導致崩潰的問題
if (urlString == nil) {
return;
}
[self.operationCache[urlString] cancel];
//從緩存池移除操作
[self.operationCache removeObjectForKey:urlString];
}
關于緩存。
對于緩存要明確明白分為內(nèi)存緩存和磁盤還盤,在調(diào)用該類執(zhí)行下載操作的時候,要首先判斷是否有緩存。有緩存就回調(diào)緩存圖片,無緩存就執(zhí)行下載。但是內(nèi)存緩存和磁盤緩存也是有一些注意的地方,判斷是否有緩存應該判斷是否有內(nèi)存緩存,如果有直接回調(diào);如果沒再去判斷是否有磁盤緩存。如果有磁盤緩存,直接回調(diào),并將磁盤緩存圖像添加到圖像緩存中,下次再去判斷這張圖片的時候就可以從內(nèi)存緩存池中讀取。如果磁盤沒有緩存,最后在開啟下載圖片任務。
//檢查是否有緩存(內(nèi)存緩存和磁盤緩存)
- (BOOL) checkImageCache:(NSString *)urlString{
//1、檢查內(nèi)存緩存
if ([self.imageCache objectForKey:urlString]) {
NSLog(@"從內(nèi)存中加載");
return YES;
}
//2、檢查沙盒緩存
UIImage *img = [UIImage imageWithContentsOfFile:[urlString appendCacheDir]];
//NSLog(@"沙盒路徑:%@",[urlString appendCacheDir]);
if (img) {
NSLog(@"從沙盒中加載 ");
//如果沙盒有圖片,要保存到內(nèi)存中============
//self.imageCache[urlString] = img;
[self.imageCache setObject:img forKey:urlString];
return YES;
}
return NO;
}
關于NSCache
NSCache使用起來基本和字典雷士,但是有一些注意點,同事功能比字典強大,因為可以設置緩存限額,當超過限額的時候,會自動移除之前的記錄,然后添加新的記錄?;臼褂镁褪撬木浯a。但是對于移除所有數(shù)據(jù)有一點值得注意的,通常在使用NSCache的時候可以在didReceiveMemoryWarning收到內(nèi)存警告方法中調(diào)用[self.cache removeAllObjects];這句代碼。調(diào)用removeAllObjects之后,就無法再次往cache中緩存數(shù)據(jù)。但是如果不是在收到內(nèi)存警告中removeAllObjects,依然是可以正常添加數(shù)據(jù)的。實際開發(fā)中應重視到這一點。
//設置數(shù)據(jù)限額
_cache.countLimit = 5;
//添加或替換數(shù)據(jù)
[self.cache setObject:@"sss" forKey:@"a"];
//根據(jù)key獲取數(shù)據(jù)
[self.cache objectForKey:@"a"];
//移除所有數(shù)據(jù)
[self.cache removeAllObjects];
1.4 關于UIImageView的分類實現(xiàn)
分類中主要有一個核心方法,設置UIImageView的圖片。直接一行代碼調(diào)用。這里同樣考慮到一點就是頻繁改變UIImageView的圖片。假設在控制器的touchBegan方法中每次點擊都會改變imageView的圖片,點擊多少次圖片就會連續(xù)切換多少次。但是加入不想讓圖片連續(xù)切換,只要顯示第一張圖片和最后一張圖片即可,其他中間觸發(fā)時間的圖片就不要顯示了,并且取消下載任務。為了滿足這個需要,所以要借助運行時的關聯(lián)對象增加屬性,記錄當前圖片的urlString地址。除了這些額外的處理外,核心就是調(diào)用操作管理類中的獲取圖片的方法。實現(xiàn)代碼如下。
- (void)zw_setImageWithUrlString:(NSString *)urlString{
//防止連續(xù)設置圖片,UIImageView上的圖片頻繁切換
//判斷當前點擊的圖片地址和上一次圖片的地址是否一樣,如果不一樣取消上一次操作
if (![urlString isEqualToString:self.currentURLString]) {
//取消上一次操作
//[self.operationCache[self.currentURLString] cancel];
[[ZWDownloderOperationManager sharedManager]cancelOperation:self.currentURLString];
}
//記錄上一次的圖片地址
self.currentURLString = urlString;
//下載圖片
[[ZWDownloderOperationManager sharedManager]downloadWithURLString:urlString finishedBlock:^(UIImage *image) {
self.image = image;
}];
}
關聯(lián)對象擴充屬性。
- (void)setCurrentURLString:(NSString *)currentURLString{
objc_setAssociatedObject(self, @"currentURLString", currentURLString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)currentURLString{
return objc_getAssociatedObject(self, @"currentURLString");
}
當然還可以在此基礎上擴展一個設置占位圖的方法,代碼如下。
- (void)zw_setImageWithUrlString:(NSString *)urlString withPlaceHolderImageName:(NSString *)placeholderStr{
self.image = [UIImage imageNamed:placeholderStr];
[self zw_setImageWithUrlString:urlString];
}
見證成果的時刻
好了,基本就這些代碼,剩下的直接在控制器中調(diào)用UIImageView分類中的方法即可,直接上效果圖啦。

二、SDWebImage框架結(jié)構(gòu)
2.1 SDWebImage中有四個核心的類,以及一些分類。
四大核心類以及其關系:
SDWebImageDownloader、SDWebImageDownloaderOperation、SDWebImageManager、SDImageCache
核心分類:
UIView+WebCacheOperation:主要在這個類中處理操作
UIButton+WebCache:button上圖片緩存
UIImage+GIF: gif圖片顯示
UIImageView+WebCache:imageView上的圖片緩存
NSData+ImageContentType:獲取文件類型
類的包含關系:
UIImageView+WebCache中包含SDWebImageManager和UIView+WebCacheOperation,
SDWebImageManager包含SDWebImageDownloader
SDWebImageManager包含SDImageCache
SDWebImageDownloader包含SDWebImageDownloaderOperation。
其中SDWebImageDownloader是負責下載的類。SDWebImageDownloaderOperation是下載操作類,繼承于NSOperation。SDWebImageDownloader中包含SDWebImageDownloaderOperation的頭文件,前者依賴于后者。
SDImageCache主要用于緩存處理,緩存處理同樣是分為內(nèi)存緩存和磁盤緩存,并定義了 SDImageCacheType枚舉用于區(qū)分緩存類型。
SDWebImageManager中主要包含了SDWebImageDownloader和SDImageCache,并且還有一個創(chuàng)建單例的方法。這個類是一個核心的管理類,將緩存和下載的業(yè)務邏輯統(tǒng)一在一起,和我們實現(xiàn)的輕量級的圖片緩存不同的是,我們將緩存的業(yè)務邏輯直接放置到管理類中,并沒有單獨抽取出來。
UIImageView+WebCache分類中包含SDWebImageManager和UIView+WebCacheOperation核心類。UIImageView+WebCache中有一個sd_cancelCurrentImageLoad方法,這個方法主要是在取消當前圖片下載,防止重復下載操作。具體實現(xiàn)放在UIView+WebCacheOperation中
整的來說,和我們之前的實現(xiàn)還是很類似的。實際上我們實現(xiàn)的是模仿SDWebImage實現(xiàn)的一個簡單的圖片緩存處理。??
另外,SDWebImageDownloader在初始化initialize的時候添加了一些通知,主要用于監(jiān)聽下載任務,顯示加載指示器。
2.2 SDWebImage的緩存
SDWebImage的緩存也是分為內(nèi)存緩存和磁盤緩存:其中內(nèi)存緩存同樣主要是通過NSCache處理。
磁盤緩存處理中會設置自動清理磁盤空間,清理周期設置為一周。SDWebImageCache中有這樣一個屬性,@property (assign, nonatomic) NSInteger maxCacheAge;該屬性默認值是一周(kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7)設置清理磁盤緩存的周期。
處理方式,請看SDWebCache中的- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock 方法。這個方法中首先看到的是異步操作,因為處理文件較多時,比較消耗資源最好是異步的方式處理。下面的代碼是定時清理磁盤緩存方法中的部分代碼,思路是獲取到一周前的時間,再通過NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]獲取文件的時間,比較兩個是時間,如果大于一周的時間,就將文件路徑添加到urlsToDelete待刪除數(shù)組中,最后遍歷這個數(shù)組,統(tǒng)一刪除過期資源。
//獲取一周前的時間
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
//用于記錄當前文件總大小
NSUInteger currentCacheSize = 0;
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
//獲取到多余一周前的時間,并添加到帶刪除的數(shù)組中
[urlsToDelete addObject:fileURL];
continue;
}
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//刪除超過一周時間的文件
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
2.3 關于SDWebImage的幾個小問題:
1、最大并發(fā)數(shù)多少?
SDWebImageDownloader.m文件中的init方法中有這樣一行代碼。即最大線程并發(fā)數(shù)為6,實際開發(fā)中并不是開啟的線程越多越好,當線程過多的時候也會影響性能,一般建議線程不要超過8前后。
_downloadQueue.maxConcurrentOperationCount = 6;
2、是否支持gif?
支持gif,主要是在UIImage+gif這個分類中。這個類中總共就只有三個方法。
self.imageView.image = [UIImage sd_animatedGIFNamed:@"1.gif"];
按照如上方式設置的gif圖片,在一些情況可能無法正常顯示gif圖片,這個是新版本SDWebImage的bug,老版本中不存在這樣的問題。設置gif圖片最好實時通過下面這個方法。
self.imageView.image = [UIImage sd_animatedGIFNamed:@"1.gif"];
NSString *filePath = [[NSBundle bundleWithPath:[[NSBundle mainBundle] bundlePath]] pathForResource:@"1.gif" ofType:nil];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
self.imageView.image = [UIImage sd_animatedGIFWithData:imageData];
3、如何判斷文件的類型?
NSData+ImageContentType.m中,sd_contentTypeForImageData方法可以獲取到文件的類型。其中 [data getBytes:&c length:1];是獲取文件的第一個字節(jié),文件的第一個字節(jié)中包含文件類型相關的信息。
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return @"image/jpeg";
case 0x89:
return @"image/png";
case 0x47:
return @"image/gif";
case 0x49:
case 0x4D:
return @"image/tiff";
case 0x52:
// R as RIFF for WEBP
if ([data length] < 12) {
return nil;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return @"image/webp";
}
return nil;
}
return nil;
}
4、磁盤緩存文件名稱是什么?
通過命名空間com.hackemist.SDWebImageCache.隔離區(qū)分。
為了防止圖片名沖突,根據(jù)MD5計算。md5的重復幾率是很小的。