SDWebImage分享
網(wǎng)絡(luò)圖片顯示大體步驟:
1.生成一個(gè)SDWebImageCombinedOperation對(duì)象(繼承與NSOperation), 添加到operation池中
2.根據(jù)URL地址, 用MD5算出散列值存儲(chǔ)在本地, 根據(jù)此散列值拼接文件路徑, 檢查沙盒中是否有緩存圖片數(shù)據(jù), 如果有, 讀出放入NSData, 然后轉(zhuǎn)換成UIImage并解碼
3.如果找不到圖片, 則啟動(dòng)下載流程下載使用NSMutableURLRequest包裝的NSOperation, 放入NSOperationQueue并發(fā)下載
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same 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();
}
});
下載圖片后的緩存層
下載完成后, 在給imageView對(duì)象賦值image的同時(shí), 內(nèi)部會(huì)有一個(gè)cache的控制類(lèi), 這個(gè)類(lèi)會(huì)創(chuàng)建一個(gè)串行的GCD隊(duì)列, 執(zhí)行文件寫(xiě)入的操作, 而且持有一個(gè)NSCache的對(duì)象, 保留最近剛下載完成的, 并且已經(jīng)解壓完成的圖片對(duì)象, 圖片在網(wǎng)絡(luò)層下載完成會(huì)會(huì)進(jìn)行decode再往上層傳遞, 在disk里讀出圖片也是同理, 所以在兩個(gè)數(shù)據(jù)層往UI層傳遞參數(shù)的時(shí)候, 這個(gè)中間的緩存層都會(huì)引用一份已經(jīng)被decode的imageData, 當(dāng)下次需要使用時(shí), 不需要再?gòu)拇疟P(pán)中讀出并decode出來(lái), 并且這個(gè)類(lèi)會(huì)配合內(nèi)存警告自動(dòng)清空ram里的圖片緩存.
寫(xiě)入磁盤(pán)
從磁盤(pán)讀取數(shù)據(jù)到內(nèi)核緩沖區(qū)
從內(nèi)核緩沖區(qū)復(fù)制到用戶(hù)空間(內(nèi)存級(jí)別拷貝)
解壓縮為位圖(耗cpu較高)
如果位圖數(shù)據(jù)不是字節(jié)對(duì)齊的,CoreAnimation會(huì)copy一份位圖數(shù)據(jù)并進(jìn)行字節(jié)對(duì)齊
CoreAnimation渲染解壓縮過(guò)的位圖
以上4,5,6,7,8步是在UIImageView的setImage時(shí)進(jìn)行的,所以默認(rèn)在主線程進(jìn)行(iOS UI操作必須在主線程執(zhí)行)
2. 一些優(yōu)化思路:
同一個(gè)URL, 但是圖片改變了已被改變:
默認(rèn)的行為, sd用的是圖片的url算出md5散列值存儲(chǔ), 所以在同一url的情況下, 得出的散列值是一樣的, 如果圖片已經(jīng)被緩存在disk上, 就會(huì)直接讀取disk上的圖而不會(huì)去刷新緩存
因此要使用SDWebImageDownloader單例里一個(gè)SDWebImageRefreshCached的策略, 并且在headersFilter中添加http的header
通過(guò)查閱HTTP協(xié)議相關(guān)的資料得知,與服務(wù)器返回的Last-Modified相對(duì)應(yīng)的request header里可以加一個(gè)名為If-Modified-Since的key,value即是服務(wù)器回傳的服務(wù)端圖片最后被修改的時(shí)間,第一次圖片請(qǐng)求時(shí)If-Modified-Since的值為空,第二次及以后的客戶(hù)端請(qǐng)求會(huì)把服務(wù)器回傳的Last-Modified值作為If-Modified-Since的值傳給服務(wù)器,這樣服務(wù)器每次接收到圖片請(qǐng)求時(shí)就將If-Modified-Since與Last-Modified進(jìn)行比較,如果客戶(hù)端圖片已陳舊那么返回狀態(tài)碼200、Last-Modified、圖片內(nèi)容,客戶(hù)端存儲(chǔ)Last-Modified和圖片;如果客戶(hù)端圖片是最新的那么返回304 Not Modified、不會(huì)返回Last-Modified、圖片內(nèi)容。
SDWebImageDownloader *imgDownloader = SDWebImageManager.sharedManager.imageDownloader;
imgDownloader.headersFilter = ^NSDictionary *(NSURL *url, NSDictionary *headers) {
NSFileManager *fm = [[NSFileManager alloc] init];
NSString *imgKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
NSString *imgPath = [SDWebImageManager.sharedManager.imageCache defaultCachePathForKey:imgKey];
NSDictionary *fileAttr = [fm attributesOfItemAtPath:imgPath error:nil];
NSMutableDictionary *mutableHeaders = [headers mutableCopy];
NSDate *lastModifiedDate = nil;
if (fileAttr.count > 0) {
if (fileAttr.count > 0) {
lastModifiedDate = (NSDate *)fileAttr[NSFileModificationDate];
}
}
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss z";
NSString *lastModifiedStr = [formatter stringFromDate:lastModifiedDate];
lastModifiedStr = lastModifiedStr.length > 0 ? lastModifiedStr : @"";
[mutableHeaders setValue:lastModifiedStr forKey:@"If-Modified-Since"];
return mutableHeaders;
};
2.1 關(guān)于異步圖片下載:
fastImageCache主要針對(duì)于從磁盤(pán)文件讀取并展示圖片的極端優(yōu)化,所以并沒(méi)有集成異步圖片下載的功能。這里主要來(lái)看看SDWebImage(AFNetWorking的基本類(lèi)似)的實(shí)現(xiàn)方案:
tableView中,異步圖片下載任務(wù)的管理:
我們知道,tableViewCell是有重用機(jī)制的,也就是說(shuō),內(nèi)存中只有當(dāng)前可見(jiàn)的cell數(shù)目的實(shí)例,滑動(dòng)的時(shí)候,新顯示cell會(huì)重用被滑出的cell對(duì)象。這樣就存在一個(gè)問(wèn)題:
一般情況下在我們會(huì)在cellForRow方法里面設(shè)置cell的圖片數(shù)據(jù)源,也就是說(shuō)如果一個(gè)cell的imageview對(duì)象開(kāi)啟了一個(gè)下載任務(wù),這個(gè)時(shí)候該cell對(duì)象發(fā)生了重用,新的image數(shù)據(jù)源會(huì)開(kāi)啟另外的一個(gè)下載任務(wù),由于他們關(guān)聯(lián)的imageview對(duì)象實(shí)際上是同一個(gè)cell實(shí)例的imageview對(duì)象,就會(huì)發(fā)生2個(gè)下載任務(wù)回調(diào)給同一個(gè)imageview對(duì)象。這個(gè)時(shí)候就有必要做一些處理,避免回調(diào)發(fā)生時(shí),錯(cuò)誤的image數(shù)據(jù)源刷新了UI。
SDWebImage提供的UIImageView擴(kuò)展的解決方案:
imageView對(duì)象會(huì)關(guān)聯(lián)一個(gè)下載列表(列表是給AnimationImages用的,這個(gè)時(shí)候會(huì)下載多張圖片),當(dāng)tableview滑動(dòng),imageView重設(shè)數(shù)據(jù)源(url)時(shí),會(huì)cancel掉下載列表中所有的任務(wù),然后開(kāi)啟一個(gè)新的下載任務(wù)。這樣子就保證了只有當(dāng)前可見(jiàn)的cell對(duì)象的imageView對(duì)象關(guān)聯(lián)的下載任務(wù)能夠回調(diào),不會(huì)發(fā)生image錯(cuò)亂。
iOS異步任務(wù)一般有3種實(shí)現(xiàn)方式:
NSOperation
GCD
NSThread
SDWebImage通過(guò)自定義NSOperation來(lái)抽象下載任務(wù)的并將下載時(shí)的數(shù)據(jù)邏輯處理統(tǒng)一放到operation中,然后結(jié)合了GCD來(lái)做一些主線程與子線程的切換邏輯。
tableview滑動(dòng)下的下載處理
從sdweb處理下載的邏輯可以知道, sd是以6個(gè)線程為最大并發(fā)數(shù)去處理下載queue, 這個(gè)跟tableview的行為其實(shí)有一定的背離. 因?yàn)楫?dāng)用戶(hù)滑動(dòng)tableview時(shí) (假設(shè)你的cell里需要下載一個(gè)圖片), 這是會(huì)不停地將下載的operation對(duì)象加入到隊(duì)列中, 當(dāng)用戶(hù)快速滑動(dòng)到列表的底部然后停下時(shí), 需要等待隊(duì)列前面大量的任務(wù)完成后才會(huì)開(kāi)始當(dāng)前cell的圖片下載, 這時(shí)就需要LIFO機(jī)制了.
sd實(shí)現(xiàn)LIFO機(jī)制的方式十分巧妙, 使用的仍然是同一個(gè)operation并發(fā)隊(duì)列并往里添加下載的operation
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
在一個(gè)并發(fā)隊(duì)列里, 最后進(jìn)入的operation會(huì)對(duì)這個(gè)即將進(jìn)入的operation添加依賴(lài), 這樣在這個(gè)新的operation的state轉(zhuǎn)為finish之前, 這個(gè)lastOperation都不會(huì)被執(zhí)行.
2.2 關(guān)于圖片解壓縮:
通用的解壓縮方案
主體的思路是在子線程,將原始的圖片渲染成一張的新的可以字節(jié)顯示的圖片,來(lái)獲取一個(gè)解壓縮過(guò)的圖片。
基本上比較流行的一些開(kāi)源庫(kù)都先后支持了在異步線程完成圖片的解壓縮,并對(duì)解壓縮過(guò)后的圖片進(jìn)行緩存。
這么做的優(yōu)點(diǎn)是在setImage的時(shí)候系統(tǒng)省去了解碼decode的步驟,缺點(diǎn)就是圖片占用的空間變大。
下面的代碼是SDWebImage的解決方案:
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (image.images) {
// Do not decode animated images
return image;
}
CGImageRef imageRef = image.CGImage;
CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
CGRect imageRect = (CGRect){.origin = CGPointZero, .size = imageSize};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
int infoMask = (bitmapInfo & kCGBitmapAlphaInfoMask);
BOOL anyNonAlpha = (infoMask == kCGImageAlphaNone ||
infoMask == kCGImageAlphaNoneSkipFirst ||
infoMask == kCGImageAlphaNoneSkipLast);
// CGBitmapContextCreate doesn't support kCGImageAlphaNone with RGB.
// https://developer.apple.com/library/mac/#qa/qa1037/_index.html
if (infoMask == kCGImageAlphaNone && CGColorSpaceGetNumberOfComponents(colorSpace) > 1) {
// Unset the old alpha info.
bitmapInfo &= ~kCGBitmapAlphaInfoMask;
// Set noneSkipFirst.
bitmapInfo |= kCGImageAlphaNoneSkipFirst;
}
// Some PNGs tell us they have alpha but only 3 components. Odd.
else if (!anyNonAlpha && CGColorSpaceGetNumberOfComponents(colorSpace) == 3) {
// Unset the old alpha info.
bitmapInfo &= ~kCGBitmapAlphaInfoMask;
bitmapInfo |= kCGImageAlphaPremultipliedFirst;
}
// It calculates the bytes-per-row based on the bitsPerComponent and width arguments.
CGContextRef context = CGBitmapContextCreate(NULL,
imageSize.width,
imageSize.height,
CGImageGetBitsPerComponent(imageRef),
0,
colorSpace,
bitmapInfo);
CGColorSpaceRelease(colorSpace);
// If failed, return undecompressed image
if (!context) return image;
CGContextDrawImage(context, imageRect, imageRef);
CGImageRef decompressedImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
UIImage *decompressedImage = [UIImage imageWithCGImage:decompressedImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(decompressedImageRef);
return decompressedImage;
}
2.4 關(guān)于第3,4點(diǎn),內(nèi)存級(jí)別拷貝
FastImageCache對(duì)這一點(diǎn)做了很大的優(yōu)化,其他的2個(gè)開(kāi)源庫(kù)則未關(guān)注這一點(diǎn)。這一塊木有深入研究,就引用一下FastImageCache團(tuán)隊(duì)對(duì)該點(diǎn)的一些說(shuō)明。
內(nèi)存映射 平常我們讀取磁盤(pán)上的一個(gè)文件,上層API調(diào)用到最后會(huì)使用系統(tǒng)方法read()讀取數(shù)據(jù),內(nèi)核把磁盤(pán)數(shù)據(jù)讀入內(nèi)核緩沖區(qū),用戶(hù)再?gòu)膬?nèi)核緩沖區(qū)讀取數(shù)據(jù)復(fù)制到用戶(hù)內(nèi)存空間,這里有一次內(nèi)存拷貝的時(shí)間消耗,并且讀取后整個(gè)文件數(shù)據(jù)就已經(jīng)存在于用戶(hù)內(nèi)存中,占用了進(jìn)程的內(nèi)存空間。
FastImageCache采用了另一種讀寫(xiě)文件的方法,就是用mmap把文件映射到用戶(hù)空間里的虛擬內(nèi)存,文件中的位置在虛擬內(nèi)存中有了對(duì)應(yīng)的地址,可以像操作內(nèi)存一樣操作這個(gè)文件,相當(dāng)于已經(jīng)把整個(gè)文件放入內(nèi)存,但在真正使用到這些數(shù)據(jù)前卻不會(huì)消耗物理內(nèi)存,也不會(huì)有讀寫(xiě)磁盤(pán)的操作,只有真正使用這些數(shù)據(jù)時(shí),也就是圖像準(zhǔn)備渲染在屏幕上時(shí),虛擬內(nèi)存管理系統(tǒng)VMS才根據(jù)缺頁(yè)加載的機(jī)制從磁盤(pán)加載對(duì)應(yīng)的數(shù)據(jù)塊到物理內(nèi)存,再進(jìn)行渲染。這樣的文件讀寫(xiě)文件方式少了數(shù)據(jù)從內(nèi)核緩存到用戶(hù)空間的拷貝,效率很高。