怎樣進(jìn)行圖片下載炸宵?
圖片下載真正的動(dòng)作是在這里
//SDWebImageDownloader.m
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
//addProgressCallback...方法主要是將progressBlock(過程回調(diào))和completedBlock(結(jié)束回調(diào))保存起來爽撒。
//以u(píng)rl為key保存到SDWebImageDownloader的URLCallbacks里面,供后面使用。
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
operation = [[wself.operationClass alloc] initWithRequest:request
inSession:self.session
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
//過程回調(diào)
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
//結(jié)束回調(diào)
}
cancelled:^{
//取消回調(diào)
}
];
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;
}
[wself.downloadQueue addOperation: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;
}
}];
return operation;
}
從代碼可以看出移怯,先是創(chuàng)建了一個(gè)SDWebImageDownloaderOperation
的實(shí)例operation
,然后把它添加到下載隊(duì)列downloadQueue
中这难。SDWebImageDownloaderOperation
繼承于NSOperation
舟误,重寫了start
方法,downloadQueue
在添加后會(huì)盡快開始執(zhí)行姻乓,去調(diào)start
方法嵌溢。(NSOperation介紹)
緊接著SD下載圖片是通過NSURLSession和NSURLSessionTask配合來完成的。NSURLSession
是在SDWebImageDownloader
里面實(shí)例化的蹋岩,然后傳入給SDWebImageDownloaderOperation
赖草,作為它的一個(gè)屬性,叫session
剪个。然后session
用來實(shí)例化dataTask
秧骑。(為了防止到這里session
還沒初始化,所以在里面又做了一層判斷扣囊,如果還沒初始化腿堤,那么我就初始化一個(gè),并且跟外面?zhèn)鬟M(jìn)來的區(qū)分開如暖,用完后也由我自己釋放笆檀。)之后啟動(dòng)任務(wù)開始下載圖片。(NSURLSession介紹)
//代碼節(jié)選自SDWebImageDownloaderOperation.m
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic) NSURLSession *ownedSession;
- (void)start {
//.......
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
//.......
[self.dataTask resume];
//.......
}
- (void)reset {
self.cancelBlock = nil;
self.completedBlock = nil;
self.progressBlock = nil;
self.dataTask = nil;
self.imageData = nil;
self.thread = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
那如何取消圖片下載呢盒至?
從上文看酗洒,取消下載圖片的線程只要調(diào)用SDWebImageDownloader
的- (void)cancelAllDownloads
方法,這個(gè)方法會(huì)調(diào)用downloadQueue
的cancelAllOperations
方法取消枷遂。
SDWebImageManager的runningOperations干嘛用的樱衷?
runningOperations
是一個(gè)數(shù)組,里面的元素是SDWebImageCombinedOperation
酒唉,這個(gè)類直接繼承NSObject矩桂,實(shí)現(xiàn)了SDWebImageOperation
(只是為了擁有一個(gè)cancel方法)。
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;
@end
runningOperations
用于標(biāo)示應(yīng)用目前有多少個(gè)正在獲取圖片的操作(記住痪伦,不是NSOperation
侄榴,也不是下載圖片這個(gè)動(dòng)作本身)。當(dāng)用戶所有正在獲取圖片的操作都不想要了的情況网沾,可以調(diào)用- (void)cancelAll
方法癞蚕,這個(gè)方法會(huì)對(duì)runningOperations
里面的子元素都執(zhí)行cancel
方法,之后清空這個(gè)數(shù)組辉哥。
//SDWebImageCombinedOperation
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}
什么桦山?這里也有個(gè)取消攒射?那跟上面講的取消圖片下載有什么關(guān)系?
我們看下面代碼
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
//...
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
//....
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//...
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
//...
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {//....}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
//....
return operation;
}
operation.cacheOperation
其實(shí)是獲取緩存的一個(gè)NSOperation
恒水,確切的說應(yīng)該是從磁盤獲取圖片会放,但是在這里并沒有像圖片下載使用到的一樣,這里只是作為一個(gè)是否取消的標(biāo)示而已钉凌。當(dāng)調(diào)用- (void)cancelAll
方法時(shí)咧最,operation.cacheOperation
的取消是為了取消緩存圖片的獲取。
但是注意到這里還有一個(gè)operation.cancelBlock
甩骏。調(diào)用- (void)cancelAll
方法是會(huì)執(zhí)行cancelBlock
的窗市。那這個(gè)是干嘛的。
上面已經(jīng)講到饮笛,下載圖片的operation
是由SDWebImageDownloader
的- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
方法返回的咨察,返回的operation
恰好是給operation.cancelBlock
里面調(diào)用。cancelBlock
執(zhí)行時(shí)福青,就直接把下載圖片的operation
給取消了摄狱。
所以SDWebImageManager
調(diào)用- (void)cancelAll
后,1會(huì)取消從磁盤加載緩存圖片无午,2會(huì)取消圖片下載動(dòng)作媒役。
緩存如何搜索到?
緩存搜索是在SDImageCache
的- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
方法中進(jìn)行的宪迟,我們來看下代碼
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
從內(nèi)存獲取緩存是指從系統(tǒng)提供的NSCache
的一個(gè)對(duì)象memCache
獲取酣衷。內(nèi)存緩存這里使用的不是集合,我想應(yīng)該是NSCache
可以設(shè)置totalCostLimit
和countLimit
次泽,這兩個(gè)屬性可以在內(nèi)存管理更加自動(dòng)化些穿仪。(NSCache介紹)
從磁盤獲取緩存的時(shí)候是先用NSOperation
實(shí)例化了一個(gè)operation
對(duì)象,operation
傳給外面意荤,用于控制磁盤獲取是否取消啊片。磁盤取到對(duì)象后會(huì)根據(jù)緩存策略決定是否將圖片保存到內(nèi)存中。
如何從磁盤找到緩存的圖片玖像?
看下面這個(gè)方法
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
上面方法獲取到data
之后紫谷,轉(zhuǎn)為image
,并根據(jù)是否解壓設(shè)置image
捐寥。重點(diǎn)的還是下面這個(gè)方法
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
if (data) {
return data;
}
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
if (imageData) {
return imageData;
}
}
return nil;
}
從方法名就可以知道是查找所有的路徑笤昨,找到data后返回。這些路徑包括defaultPath
和customPaths
上真。defaultPath
可以自定義設(shè)置咬腋,如果沒有系統(tǒng)會(huì)默認(rèn)創(chuàng)建。值得一提的是為了確保data
的唯一性睡互,SD使用CC_MD5
的方式對(duì)key
做了加密,然后用來作為文件的名字。
為毛可以控制到圖片的下載進(jìn)度呢就珠?
上文提到寇壳,SD下載圖片是通過NSURLSession
和NSURLSessionTask
配合來完成的。進(jìn)度的控制歸功于以下的方法
//response帶有目標(biāo)文件的大小妻怎,可以從這個(gè)里面獲取到壳炎。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
//只要接收到數(shù)據(jù),就會(huì)調(diào)用這個(gè)方法逼侦,所以這個(gè)方法是重復(fù)調(diào)用的匿辩。
//可以從這里獲取到單次接收了多少,然后保存到內(nèi)存變量imageData榛丢,這樣就知道已經(jīng)接收到了多少铲球。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
有了總量和單次接收量自然可以知道進(jìn)度,這時(shí)候再調(diào)用回調(diào)處理就可以了晰赞。
SD的內(nèi)存和性能處理稼病?
內(nèi)存:進(jìn)入后臺(tái)時(shí)/程序退出(UIApplicationWillTerminateNotification),會(huì)對(duì)過期圖片(什么是過期圖片掖鱼?就是已經(jīng)緩存超過maxCacheAge
時(shí)間的那些圖片)進(jìn)行刪除然走。刪除之后如果剩下圖片占有的大小大于最大大小(maxCacheSize
)的一半戏挡,那么會(huì)根據(jù)圖片修改時(shí)間排序后芍瑞,刪除舊圖片,直到大小滿足褐墅。當(dāng)程序收到內(nèi)存報(bào)警時(shí)拆檬,將內(nèi)存都刪掉。詳細(xì)代碼見- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock
方法掌栅。
性能:圖片的下載都是開新的異步線程秩仆。
//來源SDImageCache.m
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
//下面兩個(gè)方法都會(huì)調(diào)用
//- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock方法
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
聽說下載失敗后可以重新下載?
SD是有一個(gè)SDWebImageOptions
叫SDWebImageRetryFailed
猾封,但是這不意味著失敗后會(huì)自己去重新嘗試下載澄耍。
SDWebImageManager
有一個(gè)數(shù)組failedURLs
,用于存放所有下載圖片失敗的url晌缘,當(dāng)加載圖片的時(shí)候遇到上次失敗過的url并且options沒有設(shè)置為SDWebImageRetryFailed
是直接不做處理的齐莲,反之才會(huì)根據(jù)失敗的url重新加載。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
//....
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
//....
}
聽說SD里面圖片的唯一標(biāo)示可以自定義磷箕?
SDWebImageManager
提供了一個(gè)cacheKeyFilter
供程序員設(shè)置选酗,之后在要對(duì)圖片操作前,會(huì)先根據(jù)url調(diào)用以下方法獲取到唯一標(biāo)示再進(jìn)行后續(xù)操作岳枷。
- (NSString *)cacheKeyForURL:(NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return [url absoluteString];
}
}
官方給出了設(shè)置的方式
[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
}];
YYImage是如何做到逐行掃描的芒填?
一般來說呜叫,圖片下載,是通過將圖片數(shù)據(jù)轉(zhuǎn)換為NSData類型殿衰,然后順序進(jìn)行傳遞朱庆,接收到后再拼接的。YYImage的逐行掃描是因?yàn)閳D片本身有一個(gè)屬性interlace闷祥,只有設(shè)置了這個(gè)屬性才可能實(shí)現(xiàn)娱颊。(參考http://blog.csdn.net/code_for_free/article/details/51290067 )
如果本文讓你有那么點(diǎn)覺得“I get”的感覺,請(qǐng)點(diǎn)個(gè)贊唄凯砍!寫作很辛苦箱硕,路過請(qǐng)點(diǎn)贊!