上章節中遺漏了storeImage
這個方法,這章節補完,先來SDWebImageCache.h
看它的注釋:
/**
* 根據給的key將圖片存儲到內存以及磁盤(可選)緩存中
*
* @param image 需要存儲的圖片
* @param recalculate 圖片數據能否在UIImage中被構建出來
* @param imageData 從服務器返回的圖片數據
* @param key 唯一的圖片緩存key, 常使用圖片的絕對URL
* @param toDisk 設為YES時將圖片存儲進磁盤中
*/
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
通過注釋得知該方法是將圖片存儲進緩存中用的,看下里面的代碼:
if (!image || !key) {
return;
}
// 如果開啟內存緩存
if (self.shouldCacheImagesInMemory) { //< 默認是開啟的
NSUInteger cost = SDCacheCostForImage(image); //< 計算cost
[self.memCache setObject:image forKey:key cost:cost]; //< 利用內建的NSCache進行存儲
}
//如果開啟寫入磁盤
if (toDisk) {
dispatch_async(self.ioQueue, ^{ //< 存入磁盤也是在異步中進行的
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 這里主要的功能就是探測圖片是PNG還是JPEG
// We need to determine if the image is a PNG or a JPEG
// PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
// The first eight bytes of a PNG file always contain the following (decimal) values:
// 137 80 78 71 13 10 26 10
// If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
// and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 如果有圖片數據,查看前8個字節,判斷是不是png
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
[self storeImageDataToDisk:data forKey:key];
});
}
通過代碼可以知道,圖片默認是通過url作為key被存儲進NSCache中的。
if (!imageData) {
return;
}
//如果沒有對應目錄就生成一個
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 為圖片key獲取緩存路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// 轉換成NSURL
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// 關閉iCloud備份
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
如果需要將圖片緩存進磁盤中,就開一個子線程,在里面探測圖片是屬于哪種類型的,再通過storeImageDataToDisk
方法存入磁盤,默認情況下不允許iCloud備份,又學到一招。
defaultCachePathForKey
這個方法是用來生成緩存路徑的,跟蹤進去發現調用的是下面這個方法:
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
這段代碼將key丟入cachedFileNameForKey
方法中生成文件名,然后再將其追加到path后返回緩存路徑的。
我們再跟到cachedFileNameForKey
中一探究竟:
- (NSString *)cachedFileNameForKey:(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;
}
該方法將url轉為了MD5命名,目的應該是防止緩存進磁盤的文件命名沖突
回到SDWebImageDownloader.m
中,上期還沒講的downloadImageWithURL
方法:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
...
這里第一行就調用了addProgressCallback
這個方法,跟蹤進去:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
//URL用作回調字典的鍵所以不允許為nil,如果為nil立即調用completed block傳入空的image,data,error
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new]; //< URLCallbacks中沒有對應的url就創建一個NSMutableArray
first = YES;
}
//同一個URL的情況下只允許一個下載請求
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});
}
這段代碼中用到了dispatch_barrier_sync
,當寫入線程與其他寫入線程或讀取線程并行時候會產生問題,比如:
//摘自《Objective-C高級編程 iOS與OS X多線程和內存管理》
dispatch_queue_t queue = dispatch_create("barrier",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue,blk0_for_reading);
dispatch_async(queue,blk1_for_reading);
dispatch_async(queue,blk2_for_reading);
dispatch_async(queue,blk_for_writing);//可能造成讀取的數據與預期不符,如果追加多個寫入則可能發生更多問題,比如數據競爭。
dispatch_async(queue,blk3_for_reading);
dispatch_async(queue,blk4_for_reading);
dispatch_async(queue,blk5_for_reading);
將寫入任務換成dispatch_barrier_async可以解決這個問題,它會等待這個blk02并發執行完畢后,開始執行。執行完畢之后才輪到blk35并發執行
另外,關注一下self.URLCallbacks[url]
里存放的數據格。以url作為key,將completed和progress存放進去,如下所示:
{
"http://www.xxx.com/xxx.jpg" = (
{
completed = "<__NSMallocBlock__: 0x7faa6b426620>";
progress = "<__NSGlobalBlock__: 0x10a508b90>";
}
);
}
接著看addProgressCallback
方法回調部分:
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0; //< 下載超時的時間,默認是15秒。
}
在沒設置downloadTimeout
的情況下,默認超時時間為15秒
//開啟SDWebImageDownloaderUseNSURLCache選項則使用NSURL的Cache,否則忽略
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); //< 簡單地說就是請求時將cookie一起帶上
request.HTTPShouldUsePipelining = YES; //< 不等待上次請求響應,直接發送數據
if (wself.headersFilter) { //< 自定義http 頭部
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else { //< 默認http 頭部
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
這里用于構建NSMutableURLRequest以及設置http頭部信息
//wself.operationClass是Class類型,由init的時候或set時指定
//_operationClass = [SDWebImageDownloaderOperation class];
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
...
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
...
}
cancelled:^{
...
}];
這里將上面構造好的request放入SDWebImageDownloaderOperation
中被初始化。三段回調里的代碼比較長,這里拆開來說
progress:
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
//此處并未使用dispatch_barrier_sync,個人感覺是因為此處并未對sself.URLCallbacks進行修改,所以不會對后面的任務產生數據競爭(競態條件),使用dispatch_sync更恰當
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{ //< 回主線程
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; //< process
// receivedSize:下載了多少,expectedSize:預期下載量多大
// 只要有新數據塊到達,這個block就會不停地被調用
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url]; //< 任務完成后刪除對應url的回調
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; //< completed
if (callback) callback(image, data, error, finished);
}
}
cancelled:
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];//< 任務取消后刪除對應url的回調
});
}
//圖片在下載后以及在緩存中進行解壓可以提高性能但會消耗大量內存,默認YES
operation.shouldDecompressImages = wself.shouldDecompressImages;
//鑒權相關
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
//優先級相關
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//將operation加入到downloadQueue
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { //< 如果執行順序為LIFO后進先出
// 將新添加的operations作為最后一個operation的依賴,執行順序就是新添加的operation先執行,后進先出的過程,類似棧結構
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
至此本章將SDWebImageDownloader
的主要流程給介紹完了。對于網絡請求鑒權什么的我不打算深究,留著以后看完AFNetWorking
再說。
P.S:順便吐槽下簡書的代碼顯示太窄,影響閱讀效果。