在開發(fā)項目的過程中會用到很多第三方庫,比如AFNetWorking,SDWebImage,FMDB等,但一直都沒去好好的研究一下,最近剛好項目不是太緊,閑下來可以給自己充充電,先研究一下SDWebImage的底層實現(xiàn),源碼地址:SDWebImage
??先介紹一下SDWebImage,我們使用較多的是它提供的UIImageView分類,支持從遠(yuǎn)程服務(wù)器下載并緩存圖片。自從iOS5.0開始,NSURLCache也可以處理磁盤緩存,那么SDWebImage的優(yōu)勢在哪?首先NSURLCache是緩存原始數(shù)據(jù)(raw data)到磁盤或內(nèi)存,因此每次使用的時候需要將原始數(shù)據(jù)轉(zhuǎn)換成具體的對象,如UIImage等,這會導(dǎo)致額外的數(shù)據(jù)解析以及內(nèi)存占用等,而SDWebImage則是緩存UIImage對象在內(nèi)存,緩存在NSCache中,同時直接保存壓縮過的圖片到磁盤中;還有一個問題是當(dāng)你第一次在UIImageView中使用image對象的時候,圖片的解碼是在主線程中運行的!而SDWebImage會強制將解碼操作放到子線程中。下圖是SDWebImage簡單的類圖關(guān)系:

下面從UIImageView的圖片加載開始看起,Let's go!
首先我們在給UIImageView設(shè)置圖片的時候會調(diào)用方法:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
其中url為遠(yuǎn)程圖片的地址,而placeholder為預(yù)顯示的圖片。
其實還可以添加一些額外的參數(shù),比如圖片選項SDWebImageOptions
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0,//下載失敗了會再次嘗試下載
WebImageLowPriority = 1 << 1,//當(dāng)UIScrollView等正在滾動時,延遲下載圖片(放置scrollView滾動卡)
SDWebImageCacheMemoryOnly = 1 << 2,//只緩存到內(nèi)存中
SDWebImageProgressiveDownload = 1 << 3,// 圖片會邊下邊顯示
SDWebImageRefreshCached = 1 << 4,//將硬盤緩存交給系統(tǒng)自帶的NSURLCache去處理
SDWebImageContinueInBackground = 1 << 5,//后臺下載
SDWebImageHandleCookies = 1 << 6,// 通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES來處理存儲在NSHTTPCookieStore中的cookie
SDWebImageAllowInvalidSSLCertificates = 1 << 7,// 允許不受信任的SSL證書。主要用于測試目的。
SDWebImageHighPriority = 1 << 8,
SDWebImageDelayPlaceholder = 1 << 9,
SDWebImageTransformAnimatedImage = 1 << 10,
};
一般使用的是SDWebImageRetryFailed | SDWebImageLowPriority,下面看看具體的函數(shù)調(diào)用:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
{
[self sd_cancelCurrentImageLoad];//取消正在下載的操作
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//關(guān)聯(lián)該view對應(yīng)的圖片URL
/*...*/
if (url) {
__weak UIImageView *wself = self;//防止retain cricle
//由SDWebImageManager負(fù)責(zé)圖片的獲取
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
/*獲取圖片到主線層顯示*/
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
}
}
可以看出圖片是從服務(wù)端、內(nèi)存或者硬盤獲取是由SDWebImageManager管理的,這個類有幾個重要的屬性:
<pre><code>
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;//負(fù)責(zé)管理cache,涉及內(nèi)存緩存和硬盤保存
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;//負(fù)責(zé)從網(wǎng)絡(luò)下載圖片
@property (strong, nonatomic) NSMutableArray *runningOperations;//包含所有當(dāng)前正在下載的操作對象
</code></pre>
manager會根據(jù)URL先去imageCache中查找對應(yīng)的圖片,如果沒有在使用downloader去下載,并在下載完成緩存圖片到imageCache,接著看實現(xiàn):
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
{
/*...*/
//根據(jù)URL生成對應(yīng)的key,沒有特殊處理為[url absoluteString];
NSString *key = [self cacheKeyForURL:url];
//去imageCache中尋找圖片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType)
{
/*...*/
//如果圖片沒有找到,或者采用的SDWebImageRefreshCached選項,則從網(wǎng)絡(luò)下載
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
dispatch_main_sync_safe(^{
//如果圖片找到了,但是采用的SDWebImageRefreshCached選項,通知獲取到了圖片,并再次從網(wǎng)絡(luò)下載,使NSURLCache重新刷新
completedBlock(image, nil, cacheType, YES, url);
});
}
/*下載選項設(shè)置*/
//使用imageDownloader開啟網(wǎng)絡(luò)下載
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
/*...*/
if (downloadedImage && finished) {
//下載完成后,先將圖片保存到imageCache中,然后主線程返回
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
/*...*/
}
else if (image) {
//在cache中找到圖片了,直接返回
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
}
}];
return operation;
}
下面先看downloader從網(wǎng)絡(luò)下載的過程,下載是放在NSOperationQueue中進行的,默認(rèn)maxConcurrentOperationCount為6,timeout時間為15s:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak SDWebImageDownloader *wself = self;
/*...*/
//防止NSURLCache和SDImageCache重復(fù)緩存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy :NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
request.allHTTPHeaderFields = wself.HTTPHeaders;//設(shè)置http頭部
//SDWebImageDownloaderOperation派生自NSOperation,負(fù)責(zé)圖片下載工作
operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {}
cancelled:^{}];
operation.shouldDecompressImages = wself.shouldDecompressImages;//是否需要解碼
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) {
// 如果下載順序是后面添加的先運行
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
SDWebImageDownloaderOperation派生自NSOperation,通過NSURLConnection進行圖片的下載,為了確保能夠處理下載的數(shù)據(jù),需要在后臺運行runloop:
- (void)start {
/*...*/
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
//開啟后臺下載
if ([self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//在主線程發(fā)通知,這樣也保證在主線程收到通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
CFRunLoopRun();//在默認(rèn)模式下運行當(dāng)前runlooprun,直到調(diào)用CFRunLoopStop停止運行
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
下載過程中,在代理 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data中將接收到的數(shù)據(jù)保存到NSMutableData中,[self.imageData appendData:data],下載完成后在該線程完成圖片的解碼,并在完成的completionBlock中進行imageCache的緩存:
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
CFRunLoopStop(CFRunLoopGetCurrent());//停止當(dāng)前對runloop
/*...*/
if (completionBlock) {
/*...*/
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
if (!image.images) {
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];//圖片解碼
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
completionBlock(nil, nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {
completionBlock(image, self.imageData, nil, YES);
}
}
}
self.completionBlock = nil;
[self done];
}
后續(xù)的圖片緩存可以參考:SDWebImage源碼剖析(二)