SDWebImage源码分析(四)

5.SDWebImageDownloader

下面分析这个类看这个类的结构

这个类的属性比较多。

先看这个类的public 方法

1.+ (nonnull instancetype)sharedDownloader;

2.- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration;

3.- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;

4.- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field;;

5.- (void)setOperationClass:(nullable Class)operationClass;;

6.- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url

options:(SDWebImageDownloaderOptions)options

progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock

completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;;

7.- (void)cancel:(nullable SDWebImageDownloadToken *)token;;

8.- (void)setSuspended:(BOOL)suspended;

9.- (void)cancelAllDownloads;

10.- (void)createNewSessionWithConfiguration:(nonnull NSURLSessionConfiguration *)sessionConfiguration;

一共10个

接下来看初始化方法。初始化一共有三个。一个单例两个普通初始化

+ (nonnull instancetype)sharedDownloader {

static dispatch_once_t once;

static id instance;

dispatch_once(&once, ^{

instance = [self new];

});

return instance;

}

- (nonnull instancetype)init {

return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

}

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {

if ((self = [super init])) {

_operationClass = [SDWebImageDownloaderOperation class];

_shouldDecompressImages = YES;

_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;

_downloadQueue = [NSOperationQueue new];

_downloadQueue.maxConcurrentOperationCount = 6;

_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";

_URLOperations = [NSMutableDictionary new];

#ifdef SD_WEBP

_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];

#else

_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];

#endif

_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

_downloadTimeout = 15.0;

[self createNewSessionWithConfiguration:sessionConfiguration];

}

return self;

}

这里初始化,就看最后一个方法,其他的两个,最终都是调用到了,最后这个初始化

这里初始化了_operationClass ,_shouldDecompressImages,_executionOrder,_downloadQueue,_URLOperations,_HTTPHeaders,_barrierQueue,_downloadTimeout。并且调用了- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration 方法

- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {

[self cancelAllDownloads];

if (self.session) {

[self.session invalidateAndCancel];

}

sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;

/**

*  Create the session for this task

*  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate

*  method calls and completion handler calls.

*/

self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration

delegate:self

delegateQueue:nil];

}

- (void)cancelAllDownloads {

[self.downloadQueue cancelAllOperations];

}

取消该队列的所有操作,判断session属性有没有值,有值就让所有的任务无效或者cancel

重新给session 用赋值。

- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field {

if (value) {

self.HTTPHeaders[field] = value;

} else {

[self.HTTPHeaders removeObjectForKey:field];

}

}

一看就懂。给header 增加值或者删除值。

- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field {

return self.HTTPHeaders[field];

}

获取header field 的值

- (void)setOperationClass:(nullable Class)operationClass {

if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperationInterface)]) {

_operationClass = operationClass;

} else {

_operationClass = [SDWebImageDownloaderOperation class];

}

}

设置operation 类。没有就用默认的类。

这几个函数都是配置信息。不涉及主流程

下面这个函数就是重点了。下载

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url

options:(SDWebImageDownloaderOptions)options

progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock

completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

__weak SDWebImageDownloader *wself = self;

return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{

__strong __typeof (wself) sself = wself;

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 = NSURLRequestReloadIgnoringLocalCacheData;

if (options & SDWebImageDownloaderUseNSURLCache) {

if (options & SDWebImageDownloaderIgnoreCachedResponse) {

cachePolicy = NSURLRequestReturnCacheDataDontLoad;

} else {

cachePolicy = NSURLRequestUseProtocolCachePolicy;

}

}

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];

if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {

// Emulate LIFO execution order by systematically adding new operations as last operation's dependency

[sself.lastAddedOperation addDependency:operation];

sself.lastAddedOperation = operation;

}

return operation;

}];

}

这个方法调用了- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock

completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock

forURL:(nullable NSURL *)url

createCallback:(SDWebImageDownloaderOperation *(^)())createCallback方法

,给这个方法传入了三个block 。progressBlock completedBlock 两个是外界传入进来的。暂时不管

我们看第三个block  createCallback。 这个block 没有入参,返回值是SDWebImageDownloaderOperation 。

createCallback分析: 

第一步:读取downloadTimeout 属性值,要是0 就默认15秒,

第二步:缓存策略是默认是忽略当地缓存,要是options 配置SDWebImageDownloaderUseNSURLCache 和SDWebImageDownloaderUseNSURLCache 那么就配置响应的缓存策略。具体这些缓存策略具体干啥事,不在这里讨论

第三步:创建一个request,设置缓存策略和超时时间等

第四步:创建SDWebImageDownloaderOperation,根据相关参数创建相关的credential赋值给SDWebImageDownloaderOperation

第五步:将SDWebImageDownloaderOperation 加入到queue中。这里就启动SDWebImageDownloaderOperation 的下载了。

第六步:调整下载任务执行顺序。

在函数中- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url

options:(SDWebImageDownloaderOptions)options

progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock

completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock

completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock

forURL:(nullable NSURL *)url

createCallback:(SDWebImageDownloaderOperation *(^)())createCallback  返回值是调用下列函数

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock

completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock

forURL:(nullable NSURL *)url

createCallback:(SDWebImageDownloaderOperation *(^)())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 nil;

}

__block SDWebImageDownloadToken *token = nil;

dispatch_barrier_sync(self.barrierQueue, ^{

SDWebImageDownloaderOperation *operation = self.URLOperations[url];

if (!operation) {

operation = createCallback();

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) {

[self.URLOperations removeObjectForKey:url];

};

});

};

}

id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

token = [SDWebImageDownloadToken new];

token.url = url;

token.downloadOperationCancelToken = downloadOperationCancelToken;

});

return token;

}

分析这个函数

第一步:检查url 要是url为nil 那么如果completedBlock就调用completedBlock 函数。返回nil

第二步:这里用到dispatch_barrier_sync,全文件搜索,主要是保证所有的URLOperations 操作都在这个队列里面。保证对对URLOperations 执行的顺序。

这里执行搜索url 对应的 SDWebImageDownloaderOperation 要是没有找到,我们就调用createCallback 返回一个operation 并且将operation加入到URLOperations中,

第三步:给operation 增加completionBlock实现,这个实现是,判断有没有这个operation ,要是没有销毁就从 URLOperations 数组中移除。用的也是 dispatch_barrier_sync

第四步:operation 调用- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock

completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock 方法

第五步:创建一个SDWebImageDownloadToken 返回这个token。token 保存的是url 和 downloadOperationCancelToken 

这里用到了session的代理。我们看看都干啥了

- (void)URLSession:(NSURLSession *)session

dataTask:(NSURLSessionDataTask *)dataTask

didReceiveResponse:(NSURLResponse *)response

completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

// Identify the operation that runs this task and pass it the delegate method

SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];

[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];

}


都是获取operation 将 将代理传入operation操作

下面的几个回调也是。

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {

completionHandler(request);

}

重定向操作,没做任何处理。

- (void)cancel:(nullable SDWebImageDownloadToken *)token {

dispatch_barrier_async(self.barrierQueue, ^{

SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];

BOOL canceled = [operation cancel:token.downloadOperationCancelToken];

if (canceled) {

[self.URLOperations removeObjectForKey:token.url];

}

});

}

调用的是oeration的cancel :方法。取消成功,就从URLOperations 中移除移除operation

- (void)setSuspended:(BOOL)suspended {

self.downloadQueue.suspended = suspended;

}

让队列挂起。

- (void)cancelAllDownloads {

[self.downloadQueue cancelAllOperations];

}

让队列取消所有operations

- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {

[self cancelAllDownloads];

if (self.session) {

[self.session invalidateAndCancel];

}

sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;

/**

*  Create the session for this task

*  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate

*  method calls and completion handler calls.

*/

self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration

delegate:self

delegateQueue:nil];

}


这个就是重新生成一个session 

到此为止。这个类介绍完毕。这个类里面用的新类是SDWebImageDownloadToken

6.SDWebImageDownloadToken

@interface SDWebImageDownloadToken : NSObject

@property (nonatomic, strong, nullable) NSURL *url;

@property (nonatomic, strong, nullable) id downloadOperationCancelToken;

@end

这个类很简单,就两个属性,一个保存url 一个保存canceltoken。

7. SDWebImageManager

先看这个类的结构

public 和private 都有属性imageCache 和imageDownloader 。private中的是readwrite 而public 是readonly


public方法有

1.+ (nonnull instancetype)sharedManager;

2.- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;

3.- (nullable id)loadImageWithURL:(nullable NSURL *)url

options:(SDWebImageOptions)options

progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock

completed:(nullable SDInternalCompletionBlock)completedBlock;

4.- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;;

5.- (void)cancelAll;

6.- (BOOL)isRunning;

7.- (void)cachedImageExistsForURL:(nullable NSURL *)url

completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

8.- (void)diskImageExistsForURL:(nullable NSURL *)url

completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

9.- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url;

一共九个。两个初始化方法

接下来看初始化方法

+ (nonnull instancetype)sharedManager {

static dispatch_once_t once;

static id instance;

dispatch_once(&once, ^{

instance = [self new];

});

return instance;

}

- (nonnull instancetype)init {

SDImageCache *cache = [SDImageCache sharedImageCache];

SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];

return [self initWithCache:cache downloader:downloader];

}

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {

if ((self = [super init])) {

_imageCache = cache;

_imageDownloader = downloader;

_failedURLs = [NSMutableSet new];

_runningOperations = [NSMutableArray new];

}

return self;

}

一个默认初始化和两个自定义初始化。

第一个方法是单例。所有的初始化都调用到最后的这个初始化方法。

这里不做过多解释。

- (id)loadImageWithURL:(nullable NSURL *)url

options:(SDWebImageOptions)options

progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock

completed:(nullable SDInternalCompletionBlock)completedBlock

接下来看上面的这个函数。这个函数有150多行。分段贴代码

第一段

NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

断言必须要有completedBlock 

第二段

if ([url isKindOfClass:NSString.class]) {

url = [NSURL URLWithString:(NSString *)url];

}

要是url 是字符串,转换成NSURL

第三段

if (![url isKindOfClass:NSURL.class]) {

url = nil;

}


要是url不是NSURL,就为nil

第四段

__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];

__weak SDWebImageCombinedOperation *weakOperation = operation;

BOOL isFailedUrl = NO;

if (url) {

@synchronized (self.failedURLs) {

isFailedUrl = [self.failedURLs containsObject:url];

}

}

生成一个SDWebImageCombinedOperation对象。检查self.failedURLs 中是否包含url。

第五段

if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {

[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];

return operation;

}

要是没有url 并且 options 配置了SDWebImageRetryFailed 调用 上面这个函数。返回operation

第六段

@synchronized (self.runningOperations) {

[self.runningOperations addObject:operation];

}

将operation 加入到self.runningOperations 中

第七段

operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {

....

}

self.imageCache 调用的这个方法我们分析过了,就是从缓存中查找数据。

这里从disk中查找因为是异步的所以省略号部分具体执行时间不确定。

接下来看看这个block中干了啥事情

第八段

if (operation.isCancelled) {

[self safelyRemoveOperationFromRunning:operation];

return;

}

要是operation 取消掉了。那么从self.runningOperations 中移除

第九段

if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {

....

}

这里就是缓存中没找到图片获取配置刷新缓存等等,执行下列操作

第十段

if (cachedImage && options & SDWebImageRefreshCached) {

// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image

// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.

[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];

}

有缓存图片,并且配置了SDWebImageRefreshCached 回调block。意思是先回调后下载了

第十一段

SDWebImageDownloaderOptions downloaderOptions = 0;

if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;

if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;

if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;

if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;

if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;

if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;

if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;

if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;

下载图片配置

第十二段

if (cachedImage && options & SDWebImageRefreshCached) {

// force progressive off if image already cached but forced refreshing

downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;

// ignore image read from NSURLCache if image if cached but force refreshing

downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;

}

要是有图片。那就要配置成重新刷新图片。不加载进度条等。

第十三段

SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished){

....

}

下载部分就不介绍了。看block 内部

第十四段

if (!strongOperation || strongOperation.isCancelled) {

// Do nothing if the operation was cancelled

// See #699 for more details

// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data

}

要是没有这个操作了。啥也不用管

第十五段

if (error) {

[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error 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];

}

}

}

要是请求数据error完成回调,并且相关错误加入到failedURLs 变量中

第十六段

{

if ((options & SDWebImageRetryFailed)) {

@synchronized (self.failedURLs) {

[self.failedURLs removeObject:url];

}

}

BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {

// Image refresh hit the NSURLCache cache, do not call the completion block

} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {

BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];

// pass nil if the image was transformed, so we can recalculate the data from the image

[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];

}

[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

});

} else {

if (downloadedImage && finished) {

[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];

}

[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

}

}

这个分步解析

第一步:要是配置SDWebImageRetryFailed 从failedURLs 数组中移除对象。

第二步:配置刷新缓存,有cacheImage但是没有下载图片。不刷新(这里有缓存图片都是直接返回缓存图片的,接着下载的,下载下来图再另行处理)

第三步:if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]){

...

}

要是配置相关就执行相关函数吧。这里有对图片的重新处理操作,单张图片,切有代理。

切换到dispatch_get_global_queue 队列执行,并且缓存图片。

第四步:就是保存图片到缓存中。

第十七段

if (finished) {

[self safelyRemoveOperationFromRunning:strongOperation];

}

返回到imageDownloader 的block中,要是结束了,就移除strongOperation

第十八段

operation.cancelBlock = ^{

[self.imageDownloader cancel:subOperationToken];

__strong __typeof(weakOperation) strongOperation = weakOperation;

[self safelyRemoveOperationFromRunning:strongOperation];

};

给operation 赋值,block保存,取消操作。和数组移除操作

第十八段

else if (cachedImage) {

__strong __typeof(weakOperation) strongOperation = weakOperation;

[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];

[self safelyRemoveOperationFromRunning:operation];

}

要是缓存找到就回调函数

第十九段

else {

// Image not in cache and download disallowed by delegate

__strong __typeof(weakOperation) strongOperation = weakOperation;

[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];

[self safelyRemoveOperationFromRunning:operation];

}

回调结束,数组执行移除操作,我认为十九段永远不会执行。

单纯这么说看着肯定有点晕。主要是对runningOperations 操作有点晕。

我们从时间轴上来看对runningOperations 操作

大概流程是这样的。在main线程中将operation 加入到runningOperations 中。

切换到缓存的IOQueue中,要是operation操作,取消掉了,那么就移除operation。over

要是没取消掉,那么查询缓存,要是找到了。那么久从缓存中移除。没有找到。那么启动下载,切换到downloaderoperation中,下载结束,移除operation。

这个数组也反应出了函数操作的流程。


- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url {

if (image && url) {

NSString *key = [self cacheKeyForURL:url];

[self.imageCache storeImage:image forKey:key toDisk:YES completion:nil];

}

}

保存url 和image

- (void)cancelAll {    @synchronized (self.runningOperations) {        NSArray*copiedOperations = [self.runningOperations copy];

[copiedOperations makeObjectsPerformSelector:@selector(cancel)];

[self.runningOperations removeObjectsInArray:copiedOperations];

SDWebImageCombinedOperation 执行cancel操作

- (BOOL)isRunning {

BOOL isRunning = NO;

@synchronized (self.runningOperations) {

isRunning = (self.runningOperations.count > 0);

}

return isRunning;

}

检查runningOperations 数组是否为nil


- (void)cachedImageExistsForURL:(nullable NSURL *)url

completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {

NSString *key = [self cacheKeyForURL:url];

BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);

if (isInMemoryCache) {

// making sure we call the completion block on the main queue

dispatch_async(dispatch_get_main_queue(), ^{

if (completionBlock) {

completionBlock(YES);

}

});

return;

}

[self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {

// the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch

if (completionBlock) {

completionBlock(isInDiskCache);

}

}];

}

查找url缓存中有没有

- (void)diskImageExistsForURL:(nullable NSURL *)url

completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {

NSString *key = [self cacheKeyForURL:url];

[self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {

// the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch

if (completionBlock) {

completionBlock(isInDiskCache);

}

}];

}

直接到磁盘查找有灭有

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {

if (!url) {

return @"";

}

if (self.cacheKeyFilter) {

return self.cacheKeyFilter(url);

} else {

return url.absoluteString;

}

}

获取缓存url

这个类就是下载哪里比较绕。其他地方还好。

这里有个新的类SDWebImageCombinedOperation  我们看看这个类。

8.SDWebImageCombinedOperation

这个类代码较少

@interface SDWebImageCombinedOperation : NSObject@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;

@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;

@property (strong, nonatomic, nullable) NSOperation *cacheOperation;

@end


三个属性 

实现

@implementation SDWebImageCombinedOperation

- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {

// check if the operation is already cancelled, then we just call the cancelBlock

if (self.isCancelled) {

if (cancelBlock) {

cancelBlock();

}

_cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes

} else {

_cancelBlock = [cancelBlock copy];

}

}

- (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;

}

}

@end


重写CancelBlock 的set方法。

多了判断。要是当前累被标记为isCancelled 那么就直接执行cancelBlock

还有个去方法cancel

标记类isCancelled 。要是有cacheOperation 。那么也cancel。调用cancelBlock 函数。

cacheOperation 干啥用的,主要是在查询磁盘的时候,检查是否该查询操作已经被取消。

9.SDWebImageDecoder

其实我对这个类目前来看我也不是很理解。搜了下博客。这篇可能有点用。还有这篇。其实都赶不上直接cmd+shift+0 来的搜索imageNamed直接。

Discussion

This method looks in the system caches for an image object with the specified name and returns the variant of that image that is best suited for the main screen. If a matching image object is not already in the cache, this method locates and loads the image data from disk or from an available asset catalog, and then returns the resulting object.

The system may purge cached image data at any time to free up memory. Purging occurs only for images that are in the cache but are not currently being used.

In iOS 9 and later, this method is thread safe.

Special Considerations

If you have an image file that will only be displayed once and wish to ensure that it does not get added to the system’s cache, you should instead create your image usingimageWithContentsOfFile:. This will keep your single-use image out of the system image cache, potentially improving the memory use characteristics of your app.

第一段的意思是通过imageNamed的方法加载图片,首先会到系统内存中查找想要的尺寸的图片,加载没有。就到磁盘中查找。(在磁盘找到这里肯定有个操作,是加入到系统内存中),再返回结果

第二段是清除内存,可能发生在任何时候,并且清理的图片是当前没有使用的。

第三段如果图片只使用一次,那么就调用imageWithContentsOfFile:方法。

总结下imageNamed 

1. 通过这个方法加载的图片都会加载到系统内存中,自己无法管理这个内存,系统管理。

2.通过这个方法生成的图片,每次都是先从缓存中找,找不到才去磁盘找。在ios9 以后是线程安全的。

这里有篇文章 介绍png  jpg 和 bitmap图的区别

要想知道为啥有这个类就需要了解png  jpg 和bitmap的区别

我们看这个类的方法

+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image

为啥要将image 转换成 image 呢?没有vpn ,只能在百度搜索了。找到上篇文章,看出一点端倪。猜猜是不是UIimage 针对不同格式的图片,在绘制的时候不太一样呢?

这里有篇讲解博客图片内存中样式。

其实bmp是无压缩格式的图片,在内存中直接显示无需解压缩。而png 或者jpeg 都是压缩格式。在内存中也是需要先解码,再显示。由于,png 或者jpeg 格式的图片解码是在主线程进行的,所以为了省略这部分开销,我们把png 或者jpeg 格式的Image 直接转换成bmp格式的图片。

看源码:

+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {

if (![UIImage shouldDecodeImage:image]) {

return image;

}

// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.

// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];

@autoreleasepool{

CGImageRef imageRef = image.CGImage;

CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];

size_t width = CGImageGetWidth(imageRef);

size_t height = CGImageGetHeight(imageRef);

size_t bytesPerRow = kBytesPerPixel * width;

// kCGImageAlphaNone is not supported in CGBitmapContextCreate.

// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast

// to create bitmap graphics contexts without alpha info.

CGContextRef context = CGBitmapContextCreate(NULL,

width,

height,

kBitsPerComponent,

bytesPerRow,

colorspaceRef,

kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);

if (context == NULL) {

return image;

}

// Draw the image into the context and retrieve the new bitmap image without alpha

CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);

CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);

UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha

scale:image.scale

orientation:image.imageOrientation];

CGContextRelease(context);

CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;

}

}

上面先检查image 是否能decode。调用下面函数

+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {

// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error

if (image == nil) {

return NO;

}

// do not decode animated images

if (image.images != nil) {

return NO;

}

CGImageRef imageRef = image.CGImage;

CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);

BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||

alpha == kCGImageAlphaLast ||

alpha == kCGImageAlphaPremultipliedFirst ||

alpha == kCGImageAlphaPremultipliedLast);

// do not decode images with alpha

if (anyAlpha) {

return NO;

}

return YES;

}

在这里我们就需要弄懂,这个枚举的含义了。看这篇博客

这里面CGImageAlphaInfo 这个枚举值,查询好多篇文章,

typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {

kCGImageAlphaNone,              /* For example, RGB. */

kCGImageAlphaPremultipliedLast,  /* For example, premultiplied RGBA */

kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */

kCGImageAlphaLast,              /* For example, non-premultiplied RGBA */

kCGImageAlphaFirst,              /* For example, non-premultiplied ARGB */

kCGImageAlphaNoneSkipLast,      /* For example, RBGX. */

kCGImageAlphaNoneSkipFirst,      /* For example, XRGB. */

kCGImageAlphaOnly                /* No color data, alpha data only */

};

 kCGImageAlphaNone RGB模式,这个没啥好解释 3*8

kCGImageAlphaPremultipliedLast 这个就是带有A通道,a通道在最后的的RGB,这个所有RGB 已经和a 运算过 4*8

kCGImageAlphaPremultipliedFirst 带有A通道,a通道在最前面,这个所有RGB 已经和a 运算过4*8

kCGImageAlphaLast 这个就是带有A通道,a通道在最后的的RGB,这个所有RGB 已经和a 没有运算过4*8

kCGImageAlphaFirst 带有A通道,a通道在最前面,这个所有RGB 已经和a 没有运算过4*8

kCGImageAlphaNoneSkipLast 这个没有a通道,a通道位置空缺。相当与4*8 格式。前三字节是RGB 后面一位是未知的。相当于占位。方便计算机处理。

kCGImageAlphaNoneSkipFirst 这个没有a通道,a通道位置空缺。相当与4*8 格式,后三字节是RGB 后面一位是未知的。相当于占位。方便计算机处理。

kCGImageAlphaOnly 这个就是黑白模式么

所以 + (BOOL)shouldDecodeImage:(nullable UIImage *)image 返回值YES 说明是图片不带有a通道的。

接下来看看这个函数

+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {

// current

CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));

CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);

BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||

imageColorSpaceModel == kCGColorSpaceModelMonochrome ||

imageColorSpaceModel == kCGColorSpaceModelCMYK ||

imageColorSpaceModel == kCGColorSpaceModelIndexed);

if (unsupportedColorSpace) {

colorspaceRef = CGColorSpaceCreateDeviceRGB();

CFAutorelease(colorspaceRef);

}

return colorspaceRef;

}

这里又有个枚举

typedef CF_ENUM (int32_t,  CGColorSpaceModel) {

kCGColorSpaceModelUnknown = -1,

kCGColorSpaceModelMonochrome,

kCGColorSpaceModelRGB,

kCGColorSpaceModelCMYK,

kCGColorSpaceModelLab,

kCGColorSpaceModelDeviceN,

kCGColorSpaceModelIndexed,

kCGColorSpaceModelPattern

};

弄懂这里的枚举也就明白这个函数的意思了

kCGColorSpaceModelUnknown 这个就是不知道颜色空间是啥。

kCGColorSpaceModelMonochrome 就是黑白色

kCGColorSpaceModelRGB 颜色空间是RGB

kCGColorSpaceModelCMYK 颜色空间是 CMYK 什么是CMYK。这儿很多,自行查询

kCGColorSpaceModelLab 这个对颜色空间不了解,看不懂

kCGColorSpaceModelDeviceN 这个是设备支持的颜色空间

kCGColorSpaceModelIndexed 带索引的颜色空间?

kCGColorSpaceModelPattern  这个也不知道干嘛的,水平有限啊。所以这个函数我是没有看懂了。有几个颜色空间不明白。

这个函数就知道将不支持的颜色空间转换成RGB 的颜色空间,这里为啥要去掉kCGColorSpaceModelIndexed 呢?因为bitmap 传入的颜色空间不支持带索引的

colorspace

The color space to use for the bitmap context. Note that indexed color spaces are not supported for bitmap graphics contexts.

返回函数+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image

进入 @autoreleasepool 括号里面,这里面就是获取一个bitmap图。

这里关键的是创建 CGBitmapContextCreate 函数

这里是苹果官网的使用

这里有个tabble图

去掉CMYK 是因为ios 不支持。

这里代表了每个枚举值代表的含义。比方说

16 bpp, 5 bpc,kCGImageAlphaNoneSkipFirst 是16位,第一位是A,剩下的是RGB 各5位。

这就代表编码格式了。

我们创建的bitmap 模式 kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast是32位的RGBA 每位占用八个字节。

那其他参数就能说的通了。

bitsPerComponent 是 8 代表RGBA 都是 4字节 32位 对应的是RGBA_8888模式

bytesPerRow  每行占用的字节数是 4*width。

好了,其他的自行理解。


接下来看看+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image函数

这个函数调用了+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image函数

+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {

BOOL shouldScaleDown = YES;

CGImageRef sourceImageRef = image.CGImage;

CGSize sourceResolution = CGSizeZero;

sourceResolution.width = CGImageGetWidth(sourceImageRef);

sourceResolution.height = CGImageGetHeight(sourceImageRef);

float sourceTotalPixels = sourceResolution.width * sourceResolution.height;

float imageScale = kDestTotalPixels / sourceTotalPixels;

if (imageScale < 1) {

shouldScaleDown = YES;

} else {

shouldScaleDown = NO;

}

return shouldScaleDown;

}

这里判断是否需要进行压缩

static const size_t kBytesPerPixel = 4;

static const CGFloat kDestImageSizeMB = 60.0f;

static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;

static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;

static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;

static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;

看看这个值kDestTotalPixels = 60*1024*1024/4 =15M;要是图片的像素点15M ,就没必要压缩。

kTileTotalPixels = 20 * 1024*1024/4 = 5M; 

这里其实就是计算问题。那我们仔细看看计算问题。

sourceResolution 图片的实际大小

destResolution 压缩后的大小

sourceTile 宽度是图片的实际宽度 高度是 一个数。

destTile 高度是压缩的宽度。高度sourceTile * 压缩比例

我特么看晕了。

sourceSeemOverlap 是一个值。

iterations 看看原图的高比缩放图片的高的几倍。

remainder 判断原图的高比缩放图片的高是否 有余数

有余数 iterations 加1

这里的计算真的不懂为啥要这么做。请高手指教吧。到此为止。

10 SDWebImageCompat

这个类只有一个方法,inline 方法

inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) { if (!image) { return nil; } #if SD_MAC return image;#elif SD_UIKIT || SD_WATCH if ((image.images).count > 0) { NSMutableArray*scaledImages = [NSMutableArray array];

for (UIImage *tempImage in image.images) {

[scaledImages addObject:SDScaledImageForKey(key, tempImage)];

}

UIImage *animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration];

#ifdef SD_WEBP

if (animatedImage) {

SEL sd_webpLoopCount = NSSelectorFromString(@"sd_webpLoopCount");

NSNumber *value = objc_getAssociatedObject(image, sd_webpLoopCount);

NSInteger loopCount = value.integerValue;

if (loopCount) {

objc_setAssociatedObject(animatedImage, sd_webpLoopCount, @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

}

#endif

return animatedImage;

} else {

#if SD_WATCH

if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {

#elif SD_UIKIT

if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {

#endif

CGFloat scale = 1;

if (key.length >= 8) {

NSRange range = [key rangeOfString:@"@2x."];

if (range.location != NSNotFound) {

scale = 2.0;

}

range = [key rangeOfString:@"@3x."];

if (range.location != NSNotFound) {

scale = 3.0;

}

}

UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];

image = scaledImage;

}

return image;

}

#endif

}



这个其实很简单,就是读取2x 或者3x图。要是gif 那么就分别调用images里面的2x 或者3x图。重新返回一个image

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容