簡析
前段時(shí)間卧秘,和一個(gè)小伙交流鳖链,那小伙問我:
小伙:“NSString聲明屬性時(shí)姆蘸,用什么修飾?”
我:“copy”
小伙:“為什么用copy芙委,用strong有什么問題么逞敷?”
我:“如果使用strong修飾,只是對字符串做了淺拷貝,當(dāng)某個(gè)對象持有這個(gè)屬性時(shí)灌侣,會改變這個(gè)屬性值推捐。”
小伙:“那我就想讓它改變呢侧啼?”
我:“......(⊙o⊙)?”
出來混也有三年了牛柒,竟然又在最基礎(chǔ)的上面栽了,好尷尬痊乾。其實(shí)說到底還是自己內(nèi)功修為不夠焰络。蜻蜓點(diǎn)水,對于開發(fā)者而言是大忌符喝,做過幾款A(yù)PP就覺得自己怎樣怎樣闪彼,真真是井底之蛙。
做為iOS開發(fā)者协饲,相信大家都會或多或少的使用或了解過SDWebImage畏腕,剖析其源碼的文章不在少數(shù),今天我從問題驅(qū)動的角度來簡單梳理下我所理解的SDWebImage茉稠。
SDWebImages圖片類型識別問題
大家都知道描馅,UIImageView默認(rèn)情況下只能加載png類型的圖片,加載jpg/gif等類型時(shí)是要單獨(dú)處理的而线,那么SDWebImage是怎么識別網(wǎng)絡(luò)圖片的類型呢铭污?
閱讀源碼,大家會發(fā)現(xiàn)在NSData的分類文件NSData+ImageContentType.m
中膀篮,它是根據(jù)文件頭來識別嘹狞,即圖片流文件的第一個(gè)字節(jié)判斷。
#import "NSData+ImageContentType.h"
@implementation NSData (ImageContentType)
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
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:
// R as RIFF for WEBP
if (data.length < 12) {
return SDImageFormatUndefined;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return SDImageFormatWebP;
}
}
return SDImageFormatUndefined;
}
@end
OpenCV圖片類型識別也類似誓竿,參見:include1224的博客:讀文件頭判斷圖片類型
SDWebImage的下載隊(duì)列機(jī)制
SDWebImage加載網(wǎng)絡(luò)圖片的方式是異步加載的方式磅网,不管是從性能方面還是從為用戶節(jié)省流量的角度而言,SDWebImage做的都是比較好的筷屡。
那么涧偷,問題來了:
1簸喂、 異步加載多張圖片時(shí),SDWebImage是怎么處理的燎潮?是否有對應(yīng)的并發(fā)隊(duì)列喻鳄?
2、如果有确封,它的并發(fā)隊(duì)列運(yùn)行機(jī)制是怎樣的呢除呵?既然是并發(fā)隊(duì)列,最大的并發(fā)數(shù)是多少隅肥?
3竿奏、當(dāng)多個(gè)圖片下載任務(wù)結(jié)束時(shí),在隊(duì)列中移除的策略是怎樣的腥放,是先進(jìn)先出泛啸?還是后進(jìn)先出?
4秃症、當(dāng)某個(gè)圖片的URL為錯誤鏈接候址,或者服務(wù)器異常,或者網(wǎng)絡(luò)異常的情況下种柑,SDWebImage有沒有異常超時(shí)處理岗仑?如果有超時(shí)機(jī)制,時(shí)長是多少呢聚请?
下面我將一一為大家找到答案:
SDWebImage網(wǎng)絡(luò)圖片下載是通過SDWebImageDownloader和SDWebImageDownloaderOperation類來完成的荠雕。
- SDWebImageDownloaderOperation封裝了單個(gè)圖片下載操作,它有一個(gè)start方法用來開啟一個(gè)下載任務(wù)驶赏,看源碼可以看到炸卑,在start方法體中有一段:
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
在內(nèi)部明確寫了單個(gè)任務(wù)的超時(shí)時(shí)間15秒。
- SDWebImageDownloader是用來管理SDWebImageDownloaderOperation圖片下載任務(wù)的(另外在SDWebImageDownloader中也可以配置任務(wù)超時(shí)時(shí)長)煤傍,它持有多個(gè)公有屬性:maxConcurrentDownloads(最大并發(fā)數(shù))盖文、downloadTimeout(任務(wù)超時(shí)時(shí)長)、executionOrder(隊(duì)列執(zhí)行方式)等蚯姆,維護(hù)著一個(gè)私有并發(fā)下載隊(duì)列downloadQueue和一個(gè)最新任務(wù)添加任務(wù)lastAddedOperation五续。
看源碼我們可以輕松了解到,SDWebImage的下載隊(duì)列默認(rèn)情況下是SDWebImageDownloaderFIFOExecutionOrder龄恋,是先進(jìn)先出的疙驾,下載隊(duì)列并發(fā)數(shù)為6。
downloadQueue.maxConcurrentOperationCount = 6;
downloadTimeout: 15.0;
executionOrder: SDWebImageDownloaderFIFOExecutionOrder;
- 詳見源碼:
@interface SDWebImageDownloader ()
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
......
@end
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
SDWebImage緩存機(jī)制
SDWebImage緩存機(jī)制其實(shí)由兩部分組成:內(nèi)存緩存篙挽、磁盤緩存荆萤。從SDImageCache文件中我們可以清楚地看出這一點(diǎn),其中memCache即內(nèi)存緩存铣卡,diskCachePath即磁盤緩存链韭,數(shù)據(jù)文件存儲在沙盒中:
@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
......
@end
內(nèi)存緩存
先說下內(nèi)存緩存memCache,為了完善內(nèi)存緩存煮落,SDWebImage實(shí)現(xiàn)了NSCache的一個(gè)子類AutoPurgeCache敞峭,擴(kuò)充了NSCache,當(dāng)內(nèi)存警告時(shí)蝉仇,它會接受UIApplicationDidReceiveMemoryWarningNotification通知旋讹,自動執(zhí)行removeAllObjects操作。
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
@end
如果大家細(xì)心的話會發(fā)現(xiàn)轿衔,SDWebImage做了內(nèi)存緩存沉迹,當(dāng)我們頻繁的使用SDWebImage加載多張圖片時(shí),卻為何基本不會出現(xiàn)內(nèi)存暴漲的情況呢害驹?其實(shí)這一切歸功于自動釋放池@autoreleasepool鞭呕。
磁盤緩存
接下來咱們說下磁盤緩存,磁盤緩存文件是存儲在沙盒中的宛官,存儲過程比較復(fù)雜葫松。我先簡單說下,SDWebImage加載圖片的大致流程底洗,相信從中腋么,大家會對diskCache有所了解。
在使用SDWebImage時(shí)亥揖,往往是從UIImageView+WebCache文件開始的珊擂,我們使用SDWebImage第一步就是要引入U(xiǎn)IImageView的分類WebCache,然后調(diào)用sd_setImageWithURL:
方法费变,完成圖片的異步加載摧扇。
圖片加載的具體流程如下:
- 調(diào)用sd_setImageWithURL方法時(shí),它首先是通過URL作為key查詢內(nèi)存緩存胡控,即SDImageCache的memCache屬性扳剿,如果存在直接顯示到View上。
- 反之昼激,將通過md5編碼URL作為文件名庇绽,去沙盒(即SDImageCache的diskCachePath路徑下)中查詢有無此文件,如果存在橙困,就把沙盒中的文件加載到內(nèi)存緩存memCache中瞧掺,然后通過SDWebImageDecoder解碼后,直接顯示到View上凡傅。
- 如果沙盒中不存在辟狈,則先將占位圖片placeholderImage加載到View上,緊接著去SDWebImageDownloader的downloadQueue隊(duì)列中,查找是否有正在下載該圖片的下載任務(wù)哼转,如果存在繼續(xù)該任務(wù)明未。
- 如果下載隊(duì)列不存在,創(chuàng)建圖片下載任務(wù)SDWebImageDownloaderOperation壹蔓,然后通過lastAddedOperation趟妥,根據(jù)對應(yīng)的機(jī)制添加到下載并發(fā)隊(duì)列downloadQueue中,下載完畢后佣蓉,將操作在隊(duì)列中移除披摄,將圖片添加到內(nèi)存緩存中,直接顯示到View勇凭,并將該文件壓縮編碼后存儲到沙盒中疚膊,將通過md5編碼URL作為文件名。
相信看到上面的流程后虾标,大家對磁盤緩存機(jī)制有所了解寓盗,當(dāng)然也帶來了一些疑問,比如:
1夺巩、圖片文件為什么使用md5編碼的URL作為文件名贞让?
2、磁盤緩存柳譬,既然稱為緩存喳张,就肯定有一定的時(shí)間期限,緩存的時(shí)長是多少美澳?
3销部、文件過期之后,在什么時(shí)機(jī)清除過期圖片文件的制跟?
4舅桩、沙盒大小是有限度的,那么為SDWebImage預(yù)留的磁盤空間有沒有大小限制雨膨?
5擂涛、如果我想清空所有的SDWebImage緩存怎么清除?如果我們需要清除特定的圖片緩存又該怎么處理聊记?
下文撒妈,我將為大家一一解答這一系列疑問:
SDWebImage緩存圖片命名問題
SDWebImage是怎樣維護(hù)緩存圖片的?在SDImageCache文件中排监,我們不難發(fā)現(xiàn)狰右,它是利用了MD5的壓縮性特性、容易計(jì)算舆床、強(qiáng)抗碰撞等特性棋蚌,將圖片的URL進(jìn)行md5編碼嫁佳,作為文件名存儲到沙盒中的。
MD5百度百科
MD5算法具有以下特點(diǎn):
1谷暮、壓縮性:任意長度的數(shù)據(jù)蒿往,算出的MD5值長度都是固定的。
2坷备、容易計(jì)算:從原數(shù)據(jù)計(jì)算出MD5值很容易熄浓。
3情臭、抗修改性:對原數(shù)據(jù)進(jìn)行任何改動省撑,哪怕只修改1個(gè)字節(jié),所得到的MD5值都有很大區(qū)別俯在。
4竟秫、強(qiáng)抗碰撞:已知原數(shù)據(jù)和其MD5值,想找到一個(gè)具有相同MD5值的數(shù)據(jù)(即偽造數(shù)據(jù))是非常困難的跷乐。
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (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);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];
return filename;
}
SDWebImage緩存文件保留時(shí)長及緩存空間大小
既然是緩存肥败,肯定有相應(yīng)的時(shí)間期限,默認(rèn)情況下SDWebImage的緩存時(shí)長為一周愕提,并且緩存空間可以自定義馒稍。
#import "SDImageCacheConfig.h"
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
@implementation SDImageCacheConfig
- (instancetype)init {
if (self = [super init]) {
_shouldDecompressImages = YES;
_shouldDisableiCloud = YES;
_shouldCacheImagesInMemory = YES;
_maxCacheAge = kDefaultCacheMaxCacheAge;
_maxCacheSize = 0;
}
return self;
}
@end
過濾URL,禁用緩存
如果想過濾特定URL浅侨,不使用緩存機(jī)制纽谒,可以在對應(yīng)位置加入如下代碼過濾。
SDWebImageManager.sharedManager.cacheKeyFilter = ^NSString * _Nullable(NSURL * _Nullable url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
NSLog(@"url.scheme:%@, url.host:%@, url.path: %@", url.scheme, url.host, url.path);
// if([[url.host absoluteString] isEqualToString:@"upload-images.jianshu.io"])
if ([[url absoluteString] isEqualToString:@"http://upload-images.jianshu.io/upload_images/949086-5d2c51f1e3a9cddd.png"])
{
return nil;
}
return [url absoluteString];
};
清除特定圖片緩存
剛說過如输,SDWebImage加載圖片是有緩存的鼓黔,默認(rèn)存儲一周的時(shí)間。使用SDWebImage加載同樣URL的圖片時(shí)不见,優(yōu)先會從緩存中取澳化,而不是每次重新請求加載,那么問題來了稳吮,我們的頭像/廣告圖等缎谷,需要實(shí)時(shí)刷新,我們要需要清除特定的圖片緩存灶似。
單單就頭像/廣告圖更新問題而言列林,無非是更新緩存問題,有很多方法解決喻奥,
- 使用options:SDWebImageRefreshCached刷新緩存席纽,但是有些童鞋反應(yīng)該方法有閃爍問題,甚至有時(shí)并沒有更新圖片撞蚕,所以保險(xiǎn)起見润梯,最好還是手動清緩存的方式。
- 每次清除掉圖片緩存,重新加載的方式纺铭,代碼如下:
NSURL *imageURL = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/949086-5d2c51f1e3a9cddd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/999"];
// 獲取對應(yīng)URL鏈接的key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:imageURL];
NSString *pathStr = [[SDImageCache sharedImageCache] defaultCachePathForKey:key];
NSLog(@"key存儲的路徑: %@", pathStr);
// 刪除對應(yīng)key的文件
[[SDImageCache sharedImageCache] removeImageForKey:key withCompletion:^{
[self.tempImageView sd_setImageWithURL:imageURL placeholderImage:[UIImage imageNamed:@"placeholderHead.png"]];
}];
清除過期文件的時(shí)機(jī)
通過上文的解答寇钉,大家知道磁盤緩存的文件是有時(shí)間期限的,那么舶赔,SDWebImage在什么時(shí)機(jī)清除過期文件的呢扫倡?在SDImageCache文件我們同樣可以得到答案:
清除過期舊文件的時(shí)間點(diǎn)有兩處:程序切到后臺、殺死APP時(shí)竟纳。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteOldFiles) name:UIApplicationWillTerminateNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundDeleteOldFiles) name:UIApplicationDidEnterBackgroundNotification object:nil];
具體源碼如下:
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
@end
本文已在版權(quán)印備案撵溃,如需轉(zhuǎn)載請?jiān)诎鏅?quán)印獲取授權(quán)。
獲取版權(quán)