1、平時開發(fā)的過程中用到過三方庫嗎?
2、使用三方庫的過程中遇到過什么問題嗎?
3、有讀過優(yōu)秀三方的源碼么?
4、知道三方庫底層怎么實現(xiàn)的嗎?
寫在開始之前
在很多水友相親的過程中,經(jīng)常會被問到類似的問題,有些人能夠言簡意賅的把某框架的優(yōu)缺點表達出來(心中:我湊,還好我昨天背了一下,這個逼我一定要裝好),有些人卻還是停留在簡單使用API的階段,具體怎么實現(xiàn)卻支支吾吾的說不清楚(心中萬馬奔騰,麻痹的,這么底層的東西也要問嗎?)。
iOS日常開發(fā)中,常用的開源三方庫有很多AFNetworking、SDWebImage、MJRefresh、YYKit系列等,今天我們就先來說說SDWebImage。
SDWebImage的源碼第一次看還是大概2年前,當時還是用的NSURLConnection來下載圖片的,時光荏苒,SDWebImage早已改變成了NSURLSession來下載圖片,并且不斷的優(yōu)化,Github也多了1W多Star,而我還是當年那個小菜逼,說多了都是眼淚

最近一次面試的時候,被問到一個問題,
面試官:UITableView的5個cell同時下載一個相同圖片,SDWebImage底層怎么處理的?
我:之前看過,記不清了(當時我的表情是懵逼的,之前只是草草的看過一遍,而且還有很多地方都看不懂)
面試官:沒關系,如果要是你自己做,你會怎么做?
我:弄一個串行隊列,第一個任務下載完之后,緩存,然后后邊的任務就可以直接取緩存
面試官:如果我同時需要5個圖片的下載進度呢?
我:當是真心有點凌亂了,然后思(懵)考(逼)了2分鐘
面試官:好吧,今天的面試就先到這里吧
帶著面試的問題,我老老實實的又從GitHub上下載個SDWebImage-4.2.2,花了一天的時間,又看了一遍,現(xiàn)在把看明白的東西記錄一下。
1、SDWebImage工作流程


雖然SDWebImage的主要工作流程很多水友都能說出個大概,我還是簡單說一下我的理解吧,像我這種大齡程序員,說不定哪天就忘了,將來還能回來翻翻筆記。
Step1:調用加載圖片API
sd_setImageWithURL:系列
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
調用API之后,SDWebImage會判斷是否顯示占位圖,如果讓顯示就先顯示占位圖
// 如果options != SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
寫到這里再啰嗦幾句,說出來你可能不信,options & SDWebImageDelayPlaceholder這種寫法,在昨天之前我是看不懂的,看到邏輯與&,我努力回想了下當年的禿頂計算機老師是怎么講的,但是沒想起來,后來琢磨了下,應該是0 & 0 = 0, 1 & 0 = 0, 1 & 1 = 1,結合SDWebImageDelayPlaceholder的定義
* By default, placeholder images are loaded while the image is loading. This flag will delay the loading
* of the placeholder image until after the image has finished loading.
*/
SDWebImageDelayPlaceholder = 1 << 9,
1 << 9 ==> 0000 0000 0000 0001 << 9 ==> 0000 0010 0000 0000
假設options = SDWebImageRetryFailed = 1 << 0,
options & SDWebImageDelayPlaceholder
==> 0000 0000 0000 0001
& 0000 0010 0000 0000
===========================
0000 0000 0000 0000 = 0
假設options = SDWebImageDelayPlaceholder = 1 << 9
options & SDWebImageDelayPlaceholder
==> 0000 0010 0000 0000
& 0000 0010 0000 0000
===========================
0000 0010 0000 0000 = 1 << 9 = !0
option邏輯與(&)上自己本身結果是自己,非零,options邏輯與(&)上非自身的其他枚舉值,結果都是0,這個邏輯于(&)在這里跟 == 的作用是一樣的,就是判斷options的枚舉值,
options & SDWebImageDelayPlaceholder ==> options == SDWebImageDelayPlaceholder
what the fk?
原來只是個options == SDWebImageDelayPlaceholder判斷,哎,都是吃文化低的虧,啰嗦了這么多也不知道表達清楚沒。
Step2:SDImageCache以URL為key在imageCache中查找圖片
首先在memCache中查找,如果能找到image,執(zhí)行回調block,把image傳回去,如果memCache中沒有找到,會開啟一個異步線程去磁盤上查找,如果找到image,保存到memCache中,如果沒有找到,返回nil,查找完成。
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if (image.images) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
Step3:SDWebImageDownloader下載圖片
如果內(nèi)存和磁盤上都沒有查詢到URLString對應的image,就會讓imageDownloader去下載圖片,根據(jù)URL創(chuàng)建一個request,然后根據(jù)request創(chuàng)建一個sessionTask開始下載
NSTimeInterval timeoutInterval = sself.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
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[sself.downloadQueue addOperation:operation];
operation添加到operationQueue中,自動開啟下載任務,下載的過程中通過進度block回傳下載進度,下載完成后解碼轉碼,調用下載完成回調block把image對象回傳。
Step4:SDImageCache存儲圖片
默認轉碼后的圖片會緩存到內(nèi)存中,如果同時需要緩存到磁盤上,才會開啟異步IO隊列通過NSFileManager把圖片寫入到本地磁盤,磁盤上圖片的名字是經(jīng)過MD5處理后的URLString。
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, use PNG format
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
}
[self storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
[self checkIfQueueIsIOQueue];
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
這就是SDWebImage加載一張圖片的大致流程了,其實SDWebImage里面做了很多細節(jié)優(yōu)化處理。讓我們接著往下look
3、SDWebImage底層優(yōu)化
- 1、無效URL的處理
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
SDWebImageManager維護了一個黑名單存放圖片下載失敗的URL,每次根據(jù)URLString查詢圖片的時候,會先去黑名單中查詢,目標URL是否在黑名單中
BOOL isFailedUrl = NO;
if (url) {
//為了線程安全
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
如果URL在黑名單中,直接執(zhí)行回調Block,回傳error,提高效率,避免不必要的操作。如果不被黑名單包含,繼續(xù)正常流程,對應的URL下載失敗后,把URL添加到黑名單
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
- 2、高并發(fā)相同URL請求的處理
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
SD內(nèi)部每一個下載請求,對應一個SDWebImageDownloaderOperation,SDWebImageDownloader通過URLOperations屬性來維護這個operation。
dispatch_barrier_sync(self.barrierQueue, ^{
//根據(jù)URLString查詢是否有正在下載的操作
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
operation = createCallback();
//把下載操作添加到URLOperations中
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
//下完成后,把該URL的下載操作從URLOperations移除
[self.URLOperations removeObjectForKey:url];
};
});
};
}
//已經(jīng)有該URLString對應的下載任務存在,保存新任務的進度回調block和完成回調block
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
在NSURLSessionDataDelegate的- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data方法中可以看到如下代碼
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
//循環(huán)執(zhí)行相同URL的進度回調Block
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
簡單的說,就是相同的URL只開啟一個下載任務,在下載的過程中,把下載進度分別通知給該URL對應的其他操作,既節(jié)約流量、又兼顧所有任務的下載進度。下載完成的回調block同理執(zhí)行。
- 3、高并發(fā)不同URL請求的處理
UITableView的多個cell同時加載圖片的時候,就會出現(xiàn)高并發(fā)的情況
//默認最大并發(fā)數(shù)
_downloadQueue.maxConcurrentOperationCount = 6;
//session的初始化
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
系統(tǒng)API對delegateQueue參數(shù)的描述是If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
SDWebImage的高并發(fā)下載任務是在一個并行隊列,默認支持最大的并發(fā)數(shù)是6,默認并發(fā)任務執(zhí)行順序是FIFO(first in first out),如果設置任務的執(zhí)行順序為LIFO(last in first out)
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
//設置操作之間的依賴,新添加的operation被舊的operation依賴,來實現(xiàn)后進先出
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
SDWebImageDownloader在接收到NSURLSessionDataDelegate代理方法回調的時候,通過NSURLSessionDataTask獲取到對應的SDWebImageDownloaderOperation,把delegate方法轉發(fā)給SDWebImageDownloaderOperation,避免數(shù)據(jù)錯亂。
...
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
//把代理方法轉發(fā)給SDWebImageDownloaderOperation
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
//根據(jù)NSURLSessionTask獲取對應的SDWebImageDownloaderOperation
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
SDWebImageDownloaderOperation *returnOperation = nil;
for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
return returnOperation;
}
...
- 4、緩存管理策略
SDWebImage的內(nèi)存管理由SDImageCache負責,分別監(jiān)聽了內(nèi)存警告、應用將釋放、進入后臺三個通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
//收到內(nèi)存警告通知,把內(nèi)存中緩存的圖片清空
- (void)clearMemory {
[self.memCache removeAllObjects];
}
SDImageCache圖片磁盤緩存的時長默認是1周
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
在每次收到進入后臺、應用將要釋放通知后,SDWebImage會檢查磁盤上的圖片,如果過期就清理
// Remove files that are older than the expiration date;
//如果圖片過期,記錄過期圖片URL
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
//循環(huán)刪除過期圖片
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
如果設置最大的緩存空間,在收到進入后臺、應用將要釋放通知后,判斷使用當前空間使用超過設置的最大空間的50%后,開始清理,按照修改時間排序后,從修改時間最早的開始清理,直到使用空間小于緩存空間50%后結束。
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
-
5、解碼轉碼
說實話,解碼轉碼這塊看的還不太明白,等我看明白了,再回來補上...
