項目中一直都有使用SDWebImage
,對這個框架有一定的了解,但是體系卻未能貫通,因此特地整理下,主要參考:
一. 簡介
SDWebImage
提供了一個異步下載圖片并且支持緩存的UIImageView
分類。
主要邏輯為:
- 查看緩存,如果緩存中存在圖片就返回圖片并且更新
UIImageView
. - 緩存中不存在圖片就異步下載圖片,加入緩存,更新
UIImageView
.
主要用到的對象:
1、UIImageView (WebCache)
類別,入口封裝,實現讀取圖片完成后的回調
2、SDWebImageManager
,對圖片進行管理的中轉站,記錄那些圖片正在讀取。
向下層讀取Cache
(調用SDImageCache
),或者向網絡請求下載對象(調用SDWebImageDownloader
) 。
實現SDImageCache
和SDWebImageDownloader
的回調。
3、SDImageCache
,根據URL的MD5生成key對圖片進行存儲和讀取(實現存在內存中或者存在硬盤上兩種實現)
實現圖片和內存清理工作。
4、SDWebImageDownloader
,根據URL
向網絡讀取數據(實現部分讀取和全部讀取后再通知回調兩種方式)
其他類:
SDWebImageDecoder,異步對圖像進行了一次解壓。
具體流程圖:
SDWebImage
加載圖片的流程 :
入口
setImageWithURL:placeholderImage:options:
會先把placeholderImage
顯示,然后 SDWebImageManager 根據 URL 開始處理圖片。進入
SDWebImageManager-downloadWithURL:delegate:options:userInfo:
,交給SDImageCache
從緩存查找圖片是否已經下載queryDiskCacheForKey:delegate:userInfo:
.先從內存圖片緩存查找是否有圖片,如果內存中已經有圖片緩存,
SDImageCacheDelegate
回調imageCache:didFindImage:forKey:userInfo:
到SDWebImageManager
。
4.SDWebImageManagerDelegate
回調 webImageManager:didFinishWithImage:
到 UIImageView+WebCache
等前端展示圖片。
如果內存緩存中沒有,生成
NSInvocationOperation
添加到隊列開始從硬盤異步查找圖片是否已經緩存。根據
URLKey
在硬盤緩存目錄下嘗試讀取圖片文件。這一步是在 NSOperation 進行的操作,所以回主線程進行結果回調notifyDelegate:
。如果上一操作從硬盤讀取到了圖片,將圖片添加到內存緩存中(如果空閑內存過小,會先清空內存緩存)。
SDImageCacheDelegate
回調imageCache:didFindImage:forKey:userInfo:
。進而回調展示圖片。如果從硬盤緩存目錄讀取不到圖片,說明所有緩存都不存在該圖片,需要下載圖片,回調
imageCache:didNotFindImageForKey:userInfo:
。共享或重新生成一個下載器
SDWebImageDownloader
開始下載圖片。圖片下載由
NSURLConnection
來做,實現相關delegate
來判斷圖片下載中、下載完成和下載失敗。connection:didReceiveData:
中利用ImageIO
做了按圖片下載進度加載效果。connectionDidFinishLoading:
數據下載完成后交給SDWebImageDecoder
做圖片解碼處理。圖片解碼處理在一個
NSOperationQueue
完成,不會拖慢主線程UI
。如果有需要對下載的圖片進行二次處理,最好也在這里完成,效率會好很多。在主線程
notifyDelegateOnMainThreadWithInfo:
宣告解碼完成,imageDecoder:didFinishDecodingImage:userInfo:
回調給SDWebImageDownloader
。imageDownloader:didFinishWithImage: 回調給 SDWebImageManager 告知圖片下載完成。
通知所有的 downloadDelegates 下載完成,回調給需要的地方展示圖片。
將圖片保存到
SDImageCache
中,內存緩存和硬盤緩存同時保存。寫文件到硬盤也在以單獨NSInvocationOperation
完成,避免拖慢主線程。SDImageCache
在初始化的時候會注冊一些消息通知,在內存警告或退到后臺的時候清理內存圖片緩存,應用結束的時候清理過期圖片。SDWebImage
也提供了UIButton+WebCach
e 和MKAnnotationView+WebCache
,方便使用。SDWebImagePrefetcher
可以預先下載圖片,方便后續使用。
二. 架構簡介
A.架構圖:
UIImageView+WebCaceh
和UIButton+WebCache
直接為UIkit框架提供接口,而SDWebImageManger
負責處理和協調SDWebImageDownloader
和SDWebImageCache
并與UIkit
層進行交互。
三. 具體分析
1.UIImageView+WebCache
A.框架常用入口
// 所有設置圖片最終都會調用這個方法
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url
placeholderImage:placeholder
options:0
progress:nil
completed:nil];
}
該接口調用下面這個方法:
[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
該方法作為sd_setImageWithURL接口的最終入口,提供了多種參數。
url
:遠程圖片的地址placeholder
: 預顯示圖片-
options
:SDWebImageOptions
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { //下載失敗了會再次嘗試下載 SDWebImageRetryFailed = 1 << 0, //當UIScrollView等正在滾動時,延遲下載圖片(放置scrollView滾動卡) WebImageLowPriority = 1 << 1, //只緩存到內存中 SDWebImageCacheMemoryOnly = 1 << 2, // 圖片會邊下邊顯示 SDWebImageProgressiveDownload = 1 << 3, // 將硬盤緩存交給系統自帶的NSURLCache去處理 SDWebImageRefreshCached = 1 << 4, //后臺下載 SDWebImageContinueInBackground = 1 << 5, // 通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES來處理存儲在NSHTTPCookieStore中的cookie SDWebImageHandleCookies = 1 << 6, // 允許不受信任的SSL證書。主要用于測試目的。 SDWebImageAllowInvalidSSLCertificates = 1 << 7, // 默認情況下,image在裝載的時候是按照他們在隊列中的順序裝載的(就是先進先出).這個flag會把他們移動到隊列的前端,并且立刻裝載,而不是等到當前隊列裝載的時候再裝載 SDWebImageHighPriority = 1 << 8, // 默認情況下,占位圖會在圖片下載的時候顯示.這個flag開啟會延遲占位圖顯示的時間,等到圖片下載完成之后才會顯示占位圖 SDWebImageDelayPlaceholder = 1 << 9, // 是否transform圖片 SDWebImageTransformAnimatedImage = 1 << 10, };
progress
:下載進度
B.代碼分析:
操作的管理:
- (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);
// 如果選項非SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
// 設置占位圖
self.image = placeholder;
});
}
if (url.absoluteString.length > 0) {
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
// 顯示 下載轉圈
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 下載完成回調
// 移除下載進度轉圈
[wself removeActivityIndicator];
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
[self sd_cancelCurrentImageLoad]
;取消當前的下載操作,它表明 SDWebImage
管理操作的方法:
SDWebImage
所有的操作實際都是通過一個 operationDictionary
的字典管理,這個字典是動態添加到 UIView
上的一個屬性,因為這個operationDictionary
需要在UIButton
和 UIImageView
上重用,所以需要添加到它們的根類上。
這行代碼是要保證沒有當前正在進行的異步下載操作, 不會與即將進行的操作發生沖突, 它會調用:
// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
這行代碼會取消當前這個UIImageView
的所有操作,不會影響之后進行的下載操作。
占位圖的實現:
// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #4
if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;
}
當options
中沒有SDWebImageDelayPlaceholder
,UIImageView添加一個占位圖image.
獲取圖片:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #8
if (url)
檢測傳入的URL
是否為空,如果非空就調用全局的SDWebImageManager
來獲取圖片:
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
下載完成后調用(SDWebImageCompletionWithFinishedBlock)completedBlock 為 UIImageView.image 賦值, 添加上最終所需要的圖片.
// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #10
dispatch_main_sync_safe(^{
if (!wself) return;
if (image) {
wself.image = image;
[wself setNeedsLayout];
}
else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
最后在返回 operation
的同時, 也會向 operationDictionary
中添加一個鍵值對, 來表示操作的正在進行:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
它將operation 存儲到operationDictionary 中方便以后的cancel操作。
2. SDWebImageManager
這個類主要用于處理異步下載和圖片緩存的類,也可以直接用SDWebImageManager
的downloadImageWithURL:options:progress:completed:
來直接下載圖片。
可以看出這個類主要作用就是為了UIImageView+WebCache
和 SDWebImageDownloader
, SDImageCache
之間構建一個橋梁,使它們能夠更好的協同工作。
A.核心代碼分析:
a.SDWebImageManager
// SDWebImageManager
//- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
這塊代碼的功能是確定 url是否被正確傳入, 如果傳入參數的是 NSString
類型就會被轉換為NSURL
, 如果轉換失敗, 那么url
會被賦值為空, 這個下載的操作就會出錯.
b. SDWebImageCombinedOperation
當 url
被正確傳入之后, 會實例一個非常奇怪的 operation
, 它其實是一個遵循 SDWebImageOperation
協議的 NSObject
的子類. 而這個協議也非常的簡單:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
SDWebImageOperation
只是看著像NSOperation
但是它唯一跟NSOperation
相同就是都可以響應cancel
方法。調用這個類的cancel
方法,會使得它持有的兩個operation
都被cancel
。
// SDWebImageCombinedOperation
// cancel #1
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
既然獲取了url
,再通過url
獲取對應的key
.
NSString *key = [self cacheKeyForURL:url];
接著通過key在緩存中查找一起是否下載過相同的圖片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { ... }];
這里調用SDImageCache
的實例方法 queryDiskCacheForKey:done:
來嘗試在緩存中獲取圖片的數據,而這個方法獲取的就是貨真價實的NSOperation
.
如果我們在緩存中查找到對應的圖片,那么我們直接調用completedBlock
回調塊結束這一次圖片的下載操作
// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47
dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});
如果沒有找到就調用SDWebImageDownLoader的實例方法去下載該圖片:
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];
如果這個方法返回正確的downloadedImage
,那么我們就在全局緩存中存儲這個圖片的數據:
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
并調用completedBlock
對UIImageView
或者UIButton
添加圖片。
最后我們將這個subOperation
的 cancel
操作添加到operation.cancelBlock
中,方便操作的取消
operation.cancelBlock = ^{ [subOperation cancel]; }
3. SDWebImageCache
維護了一個內存緩存和一個可選的磁盤緩存,首先看下查詢圖片緩存的方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
該方法主要功能是異步查詢圖片緩存,先在內存中查找
// SDWebImageCache
// queryDiskCacheForKey:done: #9
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 內存中查找圖片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
imageFromMemoryCacheForKey:key
方法會在SDWebImageCache 維護的緩存memCache
中查找是否有對應的數據,而 memCache
就是一個 NSCache
.
NSCache
是一個類似于 NSMutableDictionary
存儲 key-value
的容器,主要有以下幾個特點:
自動刪除機制:當系統內存緊張時,NSCache
會自動刪除一些緩存對象
線程安全:從不同線程中對同一個 NSCache
對象進行增刪改查時,不需要加鎖
不同于 NSMutableDictionary
,NSCache
存儲對象時不會對key
進行 copy
操作
如果在內存中沒有找到圖片的緩存的話,就需要在磁盤中查找。
- (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;
}
}
得到圖片對應的NSData
后還有經過:
- 根據圖片的不同種類,生成對應的UIImage,
- 根據
key
值,調整image
的Scale
值 - 如果設置圖片需要解壓縮,需要對圖片進行解碼
對圖片進行存儲需要對url
進行MD5
加密計算生成相應的key
值:
- (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;
}
然后用該key
作為圖片文件名存儲在默認路徑下:
// 獲取緩存路徑方法(自己寫的)
- (NSString*)getCachePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
if (paths.count > 0) {
NSString *path = [paths[0] stringByAppendingFormat:@"/com.hackemist.SDWebImageCache.default"];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
return path;
}else{
return nil;
}
}
之前做朋友圈后臺發送圖片就是先將小圖命名,然后根據獲取到的七牛的domain和token,拼出url,接著將該url,進行md5
加密,加密后存儲到SDWebImage
的默認存儲路徑下,然后在主界面顯示存儲的小圖,后臺去進行圖片壓縮上傳任務。
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
如果在磁盤中找到圖片,就將他復制到內存中,以便下次使用。
4.SDWebImageDownloader
專用的并且優化的圖片異步下載器,主要用來下載圖片,下載放在NSOperationQueue
中進行,默認maxConcurrentOperationCount
為6,timeout時間為15s.
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
該方法直接調用了下載進度回調函數:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
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];
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();
}
});
}
方法會先查看這個 url
是否有對應的 callback
, 使用的是 downloader
,持有的一個字典URLCallbacks
.
如果是第一次添加回調的話, 就會執行first = YES
, 這個賦值非常的關鍵, 因為 first
不為 YES
那么 HTTP
請求就不會被初始化, 圖片也無法被獲取.
然后, 在這個方法中會重新修正在URLCallbacks
中存儲的回調塊.
通過dispatch_barrier_async
函數提交的任務會等它前面的任務執行完才開始,然后它后面的任務必須等它執行完畢才能開始. 必須使用dispatch_queue_create
創建的隊列才會達到上面的效果.通過該函數來保證每張圖片進度順序。
如果是第一次添加回調塊,那么就會直接運行這個createCallBack
這個block
,而這個block
,就是我們在downloadImageWithURL:options:progress:completed:
中傳入的回調塊.
接著分析下NSMutableURLRequest
請求:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
該request
發送了一個http
請求,接著又初始化一個SDWebImageDownloaderOperation
實例,這個實例用于請求網絡資源的操作,是NSOperation
的子類:
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
初始化之后,將該operation
添加到NSOperationQueue
中。(備注:NSOperation
實例只有在調用start
方法或者加入NSOperationQueue
才會執行)
[wself.downloadQueue addOperation:operation];
5.SDWebImageDownloaderOperation
這個類主要處理HTTP
請求,URL
連接的類,當這個類的實例被加入到隊列之后,start
方法被調用,start
方法首先產生一個NSURLConnection
,通過NSURLConnection
進行圖片的下載,為了確保能夠處理下載的數據,需要在后臺運行runloop
,保證程序不被掛起.
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//在主線程發通知,這樣也保證在主線程收到通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
接下來這個 connection
就會開始運行:
[self.connection start];
它發出一個SDWebImageDownloadStartNotification
通知,開啟狀態欄的請求加載轉圈。同時調用NSURLConnectionDataDelegate
代理
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;
前兩個代理會不停的回調 pregressBlock
來提示下載進度。
而最后一個代理方法會在圖片下載完成之后調用completionBlock
來完成最后 UIImageView.image
的更新,而這里調用的 progressBlock
,completionBlock
, cancelBlock
都是在之前存儲在 URLCallbacks
字典中的.
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
// 停止 該線程 運行時
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
// 通知停止狀態欄轉圈請求
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
});
}
if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}
if (completionBlock) {
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
completionBlock(nil, nil, nil, YES);
} else if (self.imageData) {
// 進行緩存
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);
}
} else {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
}
}
self.completionBlock = nil;
[self done];
}
轉換處理圖片和進行緩存后,將下載image
賦值給控件。
四. 面試點
1、SDImageCache
是怎么做數據管理的?
SDImageCache
分成兩部分,一個是內存層面的,一個是磁盤層面的。內存緩存的處理是使用
NSCache
對象來實現的。NSCache
是一個類似于集合的容器。它存儲key-value
對,這一點類似于NSDictionary
類,用搜索文件系統的方式做管理,文件替換方式是以時間為單位。我們通常用使用緩存來臨時存儲短時間使用但創建昂貴的對象。重用這些對象可以優化性能,因為它們的值不需要重新計算。另外一方面,這些對象對于程序來說不是緊要的,在內存緊張時會被丟棄。磁盤緩存的處理則是使用
NSFileManager
對象來實現的。圖片存儲的位置是位于Cache
文件夾,文件替換方式是以時間為單位,剔除時間大一一周的圖片文件。當
SDWebImageManager
向SDImageCache
要資源時, 先搜索內存層面的數據,如果有直接返回,沒有再訪問磁盤,如果有將圖片從磁盤讀取出來,然后做解壓,將圖片對象放到內存層面做備份,再返回調用層。
- 為什么圖片要進行解壓?
- 因為
UIImage
的imageWithData
函數是每次畫圖的時候才將Data
解壓成ARGB
圖像,所以在每次畫圖的時候,會有一個解壓操作,這樣效率很低,但是只有瞬時的內存需求,為了提高效率,通過SDWebImageDecoder
將包裝在Data
下的資源解壓,然后畫在另外一張圖片上面,這樣這張圖片就不需要重復解壓了,這種做法就是典型的空間換取時間的做法。
3.SDWebImage
在多線程下載圖片時防止錯亂的策略
SDWebImage
會將ImageView
對象關聯一個下載列表(列表是給AnimationImages
用的,這個時候會下載多張圖片),當tableView滾動時,imageView會重設數據源url
,這時會cancel掉下載列表中當前對應的下載任務,然后開啟一個新的下載任務,這樣就保證只有當前可見的cell
對象的ImageView
對象關聯的下載任務能夠回調,不會發生Image
錯亂。同時,
SDWebImage
管理了一個全局下載隊列SDWebDownloadManager
,并發量設置為6,也就表示如果cell
的數目大于6,就會有部分下載隊列處于等待狀態,而且,在添加下載任務到全局的下載隊列中去的時候,SDWebImage
默認采取的是LIFO(后進先出)策略,具體是添加新的下載任務的時候,將之前的下載任務添加依賴為新的下載任務。
另外解決方案:
將
imageView
對象和圖片的url
相關聯,在滑動時,不取消舊的下載任務,而是在下載任務完成回調時,進行url
匹配,只有匹配成功的image
會刷新imageView
對象,而其他的image
則只做緩存操作,而不刷新UI
。同時,仍然管理一個執行隊列,為了避免占用太多的資源,通常會對執行隊列設置一個最大的并發量。此外,為了保證
LIFO
的下載策略,可以自己維持一個等待隊列,每次下載任務開始的時候,將后進入的下載任務插入到等待隊列的前面。
-
SDWebImage
的主要任務就是圖片的下載和緩存。為了支持這些操作,它主要使用了以下知識點:
dispatch_barrier_sync
函數:該方法用于對操作設置屏障,確保在執行完任務后才會執行后續操作。該方法常用于確保類的線程安全性操作。NSMutableURLRequest
:用于創建一個網絡請求對象,我們可以根據需要來配置請求報頭等信息。
NSOperation
及NSOperationQueue
:操作隊列是Objective-C
中一種高級的并發處理方法,現在它是基于GCD
來實現的。相對于GCD
來說,操作隊列的優點是可以取消在任務處理隊列中的任務,另外在管理操作間的依賴關系方面也容易一些。對SDWebImage
中我們就看到了如何使用依賴將下載順序設置成后進先出的順序。
-
NSURLConnection
:用于網絡請求及響應處理。在iOS7.0
后,蘋果推出了一套新的網絡請求接口,即NSURLSession
類。
開啟一個后臺任務。
NSCache
類:一個類似于集合的容器。它存儲key-value對,這一點類似于NSDictionary類。我們通常用使用緩存來臨時存儲短時間使用但創建昂貴的對象。重用這些對象可以優化性能,因為它們的值不需要重新計算。另外一方面,這些對象對于程序來說不是緊要的,在內存緊張時會被丟棄。清理緩存圖片的策略:特別是最大緩存空間大小的設置。如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實際大小小于我們設置的最大使用空間。
對圖片的解壓縮操作:這一操作可以查看
SDWebImageDecoder.m
中+decodedImageWithImage
方法的實現。對
GIF
圖片的處理對
WebP
圖片的處理
- 系統級內存警告如何處理
- 取消當前正在進行的所有下載操作
[[SDWebImageManager sharedManager] cancelAll];
- 清除緩存數據:
內存緩存:直接刪除文件,重新創建新的文件
磁盤緩存:刪除過期的文件數據,計算當前未過期的已經下載的文件數據的大小,如果發現該數據大小大于我們設置的最大緩存數據大小,那么程序內部會按照按文件數據緩存的時間從遠到近刪除,知道小于最大緩存數據為止。
- 如何播放
gif
圖片
- 把用戶傳入的gif圖片->
NSData
- 根據該
Data
創建一個圖片數據源(NSData
->CFImageSourceRef
) - 計算該數據源中一共有多少幀,把每一幀數據取出來放到圖片數組中
- 根據得到的數組+計算的動畫時間-》可動畫的image
- [UIImage animatedImageWithImages:images duration:duration];
- 如何判斷當前圖片類型
+ (NSString *)sd_contentTypeForImageData:(NSData *)data;
圖片的十六進制數據, 的前8個字節都是一樣的, 所以可以通過判斷十六進制來判斷圖片的類型
五. 最后
送上一張自己喜歡的圖片:
個人小結,有興趣的朋友可以看一下,如果覺得不錯,麻煩給個喜歡或star,若發現問題請及時反饋,謝謝!