SDWebImage源碼淺析

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工作流程

這是GitHub上提供的,我順手給牽了過來,再說了,文化人的事,能叫偷么?

雖然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、解碼轉碼


說實話,解碼轉碼這塊看的還不太明白,等我看明白了,再回來補上...

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容