【iOS】—— SDWebImage源码学习

2023-11-15

SDWebImage源码


SDWebImage是一个从远程服务器下载和缓存图片的第三方库,我们来看看它的整个实现流程:
请添加图片描述
请添加图片描述

1.UIKit层

我们先来看UIKit层,最外层的UIImageView+WebCache层给我们提供了很多接口:

//UIImageView+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:nil
                            progress:progressBlock
                           completed:completedBlock];
}

- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
                                 placeholderImage:(nullable UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(nullable SDExternalCompletionBlock)completedBlock {
    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
    UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
    
    [self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];    
}

这些方法最后都会调用到:

//UIImageView+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

继续往下走,到了UIView+WebCache层:

//UIView+WebCache
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil];
}

//参数一:url            图像的url。
//参数二:placeholder    最初要设置的图像,直到图像请求完成。
//参数三:options        下载图像时要使用的选项。
//参数四:context        上下文包含用于执行指定更改或过程的不同选项
//参数五:setImageBlock  块用于自定义设置图像代码。
//参数六:progressBlock  下载图像时调用的块。进度块是在后台队列上执行的。
/*这个块没有返回值,并以请求的UIImage作为第一个参数,NSData表示作为第二个参数。
如果发生错误,image参数为nil,第三个参数可能包含一个NSError。
第四个参数是一个“SDImageCacheType”enum,表示图像是从本地缓存、内存缓存还是网络检索到的。
第五个参数通常总是“YES”。但是,如果你提供SDWebImageAvoidAutoSetImage与SDWebImageProgressiveLoad选项,以启用渐进下载和设置自己的映像。因此,对部分图像重复调用该块。当图像完全下载后,最后一次调用该块,并将最后一个参数设置为YES。
最后一个参数是原始图像URL。*/

为什么会调用到UIView+WebCache?
因外SDWebImage不仅仅支持UIImageView的图片加载,还同样支持UIButton等控件的加载。

接下来看看这个函数的实现过程:

sd_internalSetImageWithURL

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {
    if (context) {
        //获取枚举选项
        // copy to avoid mutable object
        // 复制以避免可变对象
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    //获取一个有效的操作的对应键值,这里获取的是"设置图片操作的键"
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    //如果传递的context中没有该图片操作键,就新建一个该类的操作键赋给context
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        // 将操作键传递给下游,可用于跟踪操作或图像视图类
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    
    //将当前操作键赋值给 sd_latestOperationKey属性,操作键用于标识一个视图实例的不同查询(如UIButton)。
    self.sd_latestOperationKey = validOperationKey;
    //下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    //将传递过来的url赋值给 sd_imageURL属性
    //注意,由于类别的限制,如果直接使用setImage:,这个属性可能会不同步。
    self.sd_imageURL = url;
    
    //添加临时的占位图(在不延迟添加占位图的option下)
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    if (url) {
        // reset the progress
        //重置进程
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC     //环境判断
        // check and start image indicator
        // 检查并启动图像指示灯
        [self sd_startImageIndicator]; //开启图像指示器
        
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
        //self.sd_imageIndicator
        //图像加载期间的图像指示器。如果你不需要指示器,请指定nil。默认为零
        //这个设置将移除旧的指示器视图,并将新的指示器视图添加到当前视图的子视图中。
        //@注意,因为这是UI相关的,你应该只从主队列访问。

#endif  //环境判断
        //从context字典中获取该键对应的数据
        //SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
        //如果不存在,就新创建一个单例
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        } else {
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            // 删除manager以避免循环引用(manger -> loader -> operation -> context -> manager)
            SDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[SDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }
        
        //--------------SDImageLoaderProgressBlock-----------------//
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                //项目总数
                imageProgress.totalUnitCount = expectedSize;
                //已完成数
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC//环境判断
            //重写并更新进度
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                //在主线程不等待执行
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif      //环境判断
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        //--------------SDImageLoaderProgressBlock-----------------//
        @weakify(self);
        //SDWebImageManager下载图片
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @strongify(self);
            //如果不存在就直接,结束函数了。
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            // 如果进度没有更新,将其标记为完成状态
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            // 检查和停止图像指示灯,如果完成了就停止
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            //是否应该调用CompletedBlock,通过options和状态的&运算决定
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            //是否应该不设置Image,通过options和状态的&运算决定
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            //调用CompletedBlock的Block
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!self) { return; }
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                //判断是否需要调用completedBlock
                if (completedBlock && shouldCallCompletedBlock) {
                    //调用completedBlock,image,而且不自动替换 placeholder image
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set;我们得到了一个图像,但是SDWebImageAvoidAutoSetImage标志被设置了
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set;我们没有得到图像,SDWebImageDelayPlaceholder没有设置
            if (shouldNotSetImage) {     //如果应该不设置图像
                //主线程不等待安全的调用上述创建的callCompletedBlockClojure
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;//结束函数
            }
            
            UIImage *targetImage = nil;//目标图像
            NSData *targetData = nil;//目标数据
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                //我们得到一个图像,SDWebImageAvoidAutoSetImage没有设置
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                //我们没有图像,并且设置了SDWebImageDelayPlaceholder标志
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // 检查是否使用图像过渡
            SDWebImageTransition *transition = nil;
            BOOL shouldUseTransition = NO;//是否使用过滤属性
            if (options & SDWebImageForceTransition) {//强制转换
                // 总是
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) { //类型不可用
                // 从网络
                shouldUseTransition = YES;
            } else {
                // From disk (and, user don't use sync query)
                // 从磁盘(并且,用户不使用同步查询)
                if (cacheType == SDImageCacheTypeMemory) { //内存
                    shouldUseTransition = NO;
                } else if (cacheType == SDImageCacheTypeDisk) { //磁盘
                    if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
                        shouldUseTransition = NO;
                    } else {
                        shouldUseTransition = YES;
                    }
                } else {
                    // Not valid cache type, fallback
                    // 缓存类型无效,请回退
                    shouldUseTransition = NO;
                }
            }
            //完成并且应该使用转换
            if (finished && shouldUseTransition) {
                transition = self.sd_imageTransition;   //转换属性,图片加载完成后的图片转换
            }
#endif
            //dispatch_main_async_safe : 保证block能在主线程安全进行,不等待
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                //马上替换 placeholder image
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                //马上替换 placeholder image
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                //使用上述创建的block变量,调用CompletedBlock
                callCompletedBlockClojure();
            });
        }];
        //----------operation----------//
        //在操作缓存字典(operationDictionary)里添加operation,表示当前的操作正在进行
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
        //-------------------------url存在---------------------------//
    } else {
//-------------------------url不存在---------------------------//
#if SD_UIKIT || SD_MAC
        //url不存在立马停止图像指示器
        [self sd_stopImageIndicator];
#endif
        //如果url不存在,就在completedBlock里传入error(url为空)
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
    //-------------------------url不存在---------------------------//
}

我们来分开分别看一下:

1.取消之前下载操作
    if (context) {
        //获取枚举选项
        // copy to avoid mutable object
        // 复制以避免可变对象
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    //获取一个有效的操作的对应键值,这里获取的是"设置图片操作的键"
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    //如果传递的context中没有该图片操作键,就新建一个该类的操作键赋给context
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        // 将操作键传递给下游,可用于跟踪操作或图像视图类
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    
    //将当前操作键赋值给 sd_latestOperationKey属性,操作键用于标识一个视图实例的不同查询(如UIButton)。
    self.sd_latestOperationKey = validOperationKey;
    //下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];

其中在这一部分调用了UIView+WebCacheOperation中的方法,我们来看看它是怎么实现的:

//设置图像加载操作,使用一个字典存储key和对应的操作
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    //key存在
    if (key) {
        //取消当前UIView和key的所有操作,key——标识操作的键
        [self sd_cancelImageLoadOperationWithKey:key];
        //操作存在
        if (operation) {
            //获取operationDictionary字典
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            //创建一个互斥锁,保证此时没有其它线程对self对象进行修改,然后再添加key和对应的operation(操作)
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

实际上,所有的操作都是由一个实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。
为什么不直接在UIImageView+WebCache里直接关联这个对象呢?我觉得这里作者应该是遵从面向对象的单一职责原则(SRP:Single responsibility principle),就连类都要履行这个职责,何况分类呢?这里作者专门创造一个分类UIView+WebCacheOperation来管理操作缓存(字典)。

2.设置占位图
//2.设置占位图
    //将传递过来的url赋值给 sd_imageURL属性
    //注意,由于类别的限制,如果直接使用setImage:,这个属性可能会不同步。
    self.sd_imageURL = url;
    
    //添加临时的占位图(在不延迟添加占位图的option下)
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
3.判断URL是否合法
    //3.判断URL是否合法
    if (url) {
        // reset the progress
        //重置进程
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC     //环境判断
        // check and start image indicator
        // 检查并启动图像指示灯
        [self sd_startImageIndicator]; //开启图像指示器
        
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
        //self.sd_imageIndicator
        //图像加载期间的图像指示器。如果你不需要指示器,请指定nil。默认为零
        //这个设置将移除旧的指示器视图,并将新的指示器视图添加到当前视图的子视图中。
        //@注意,因为这是UI相关的,你应该只从主队列访问。

#endif  //环境判断
        //从context字典中获取该键对应的数据
        //SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
        //如果不存在,就新创建一个单例
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        } else {
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            // 删除manager以避免循环引用(manger -> loader -> operation -> context -> manager)
            SDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[SDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }
        
        //--------------SDImageLoaderProgressBlock-----------------//
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                //项目总数
                imageProgress.totalUnitCount = expectedSize;
                //已完成数
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC//环境判断
            //重写并更新进度
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                //在主线程不等待执行
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif      //环境判断
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        //--------------SDImageLoaderProgressBlock-----------------//
        @weakify(self);
        //SDWebImageManager下载图片
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @strongify(self);
            //如果不存在就直接,结束函数了。
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            // 如果进度没有更新,将其标记为完成状态
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            // 检查和停止图像指示灯,如果完成了就停止
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            //是否应该调用CompletedBlock,通过options和状态的&运算决定
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            //是否应该不设置Image,通过options和状态的&运算决定
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            //调用CompletedBlock的Block
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!self) { return; }
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                //判断是否需要调用completedBlock
                if (completedBlock && shouldCallCompletedBlock) {
                    //调用completedBlock,image,而且不自动替换 placeholder image
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set;我们得到了一个图像,但是SDWebImageAvoidAutoSetImage标志被设置了
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set;我们没有得到图像,SDWebImageDelayPlaceholder没有设置
            if (shouldNotSetImage) {     //如果应该不设置图像
                //主线程不等待安全的调用上述创建的callCompletedBlockClojure
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;//结束函数
            }
            
            UIImage *targetImage = nil;//目标图像
            NSData *targetData = nil;//目标数据
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                //我们得到一个图像,SDWebImageAvoidAutoSetImage没有设置
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                //我们没有图像,并且设置了SDWebImageDelayPlaceholder标志
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // 检查是否使用图像过渡
            SDWebImageTransition *transition = nil;
            BOOL shouldUseTransition = NO;//是否使用过滤属性
            if (options & SDWebImageForceTransition) {//强制转换
                // 总是
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) { //类型不可用
                // 从网络
                shouldUseTransition = YES;
            } else {
                // From disk (and, user don't use sync query)
                // 从磁盘(并且,用户不使用同步查询)
                if (cacheType == SDImageCacheTypeMemory) { //内存
                    shouldUseTransition = NO;
                } else if (cacheType == SDImageCacheTypeDisk) { //磁盘
                    if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
                        shouldUseTransition = NO;
                    } else {
                        shouldUseTransition = YES;
                    }
                } else {
                    // Not valid cache type, fallback
                    // 缓存类型无效,请回退
                    shouldUseTransition = NO;
                }
            }
            //完成并且应该使用转换
            if (finished && shouldUseTransition) {
                transition = self.sd_imageTransition;   //转换属性,图片加载完成后的图片转换
            }
#endif
            //dispatch_main_async_safe : 保证block能在主线程安全进行,不等待
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                //马上替换 placeholder image
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                //马上替换 placeholder image
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                //使用上述创建的block变量,调用CompletedBlock
                callCompletedBlockClojure();
            });
        }];
        //----------operation----------//
        //在操作缓存字典(operationDictionary)里添加operation,表示当前的操作正在进行
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
        //-------------------------url存在---------------------------//
    } else {
//-------------------------url不存在---------------------------//
#if SD_UIKIT || SD_MAC
        //url不存在立马停止图像指示器
        [self sd_stopImageIndicator];
#endif
        //如果url不存在,就在completedBlock里传入error(url为空)
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
    //-------------------------url不存在---------------------------//

2.工具层

上文提到过,SDWebImageManager同时管理SDImageCache和SDWebImageDownloader两个类,它是这一层的老大哥。在下载任务开始的时候,SDWebImageManager首先访问SDImageCache来查询是否存在缓存,如果有缓存,直接返回缓存的图片。如果没有缓存,就命令SDWebImageDownloader来下载图片,下载成功后,存入缓存,显示图片。以上是SDWebImageManager大致的工作流程。

直接找到loadImageWithURL方法,这个方法主要是对url的一些判断,context与options的预处理,内容如下:
a. 先判断url的可行性
b. 对context,options进行预处理,并放到result里面
c. 调用callCacheProcessForOperation 判断是否有缓存,如果有则进入ImageCache 拿到缓存数据,如果没有则进入callDownloadProcessForOperation 方法进一步判断如何下载
先看看这些步骤的源码,看完再看callCacheProcessForOperation做了些什么

SDWebImageManager

SDWebImageManager属性

 // ==============  SDWebImageManager.m的扩展属性声明部分 ============== //
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache; //管理缓存
@property (strong, nonatomic, readwrite, nonnull) id<SDImageLoader> imageLoader; //下载器imageLoader;
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs; //记录失效url的名单
@property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations; //记录当前正在执行的操作

SDWebImageManager下载图片的方法只有一个:

- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                 completed:(nonnull SDInternalCompletionBlock)completedBlock;
//如果缓存中没有图像,则在给定URL下载图像,否则返回缓存版本。
//参数一:url            图像的URL
//参数二:options        指定此请求使用的选项的掩码
//参数三:context        上下文包含不同的选项来执行指定的更改或流程,参见' SDWebImageContextOption '。这将保存'options'枚举不能保存的额外对象。
//参数四:progressBlock  下载图像时调用的块,进度块在后台队列上执行
//参数五:completedBlock 当操作完成时调用的块
     //必选参数
     /*   这个块没有返回值,并以请求的UIImage作为第一个参数,NSData表示作为第二个参数。
 	 *   如果发生错误,image参数为nil,第三个参数可能包含一个NSError。
 	 *   第四个参数是一个“SDImageCacheType”枚举,指示是否从本地缓存检索到图像或者从内存缓存或者网络。
 	 *   当使用SDWebImageProgressiveLoad选项并下载镜像时,第五个参数设置为NO。因此,对部分图像重复调用该块。当图像完全下载后,最后一次调用该块,并将最后一个参数设置为YES。
	 *   最后一个参数是原始图像URL
     //return 返回一个SDWebImageCombinedOperation的实例,你可以取消加载过程。

我们来看下它的实现:

loadImageWithURL
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    // 没有completedBlock调用这个方法是没有意义的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    //很常见的错误是使用NSString对象而不是NSURL发送URL。由于一些奇怪的原因,Xcode不会对这种类型不匹配抛出任何警告。这里我们通过允许url作为NSString传递来避免这个错误。
    //如果url是NSString类,那么就将它转为NSURL类
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    // 防止应用程序在参数类型错误时崩溃,比如发送NSNull而不是NSURL
    //如果url不是NSURL类那么就赋值为nil
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    //SDWebImageCombinedOperation表示缓存和加载器操作的组合操作。您可以使用它来取消加载过程。
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    //让上述变量的manager指向该类
    operation.manager = self;

    //判断该URL是否在黑名单中
    BOOL isFailedUrl = NO;
    //如果url存在
    if (url) {
        //线程等待self.failedURLsLock执行,知道其执行完毕
        SD_LOCK(self.failedURLsLock);
        //在self.failedURLs(失效的url名单)中查找是否有这个url
        isFailedUrl = [self.failedURLs containsObject:url];
        //使传入的信号量self.failedURLsLock的调用值加1,表示当前又加入一个线程等待其处理的信号量
        SD_UNLOCK(self.failedURLsLock);
    }

    //url的绝对字符串长度为0或者(不禁用黑名单)并且Url在黑名单内,发出警告提示
    //url有问题的话该函数就在这就结束了
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
        //调用completionBlock块,结束该函数
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
        return operation;
    }

    //将这个操作加入进程
    SD_LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(self.runningOperationsLock);
    
    // Preprocess the options and context arg to decide the final the result for manager
    // 对操作和上下文参数进行预处理,为manager确定最终结果
        //返回已处理的操作结果,包括指定图像URL的操作和context
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    
    // Start the entry to load image from cache
    // 启动从缓存加载图像的条目
    //查询普通缓存进程
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
    //返回这个新创建的操作变量
    return operation;
}

在最后两步调用了两个方法:- (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context;(返回已处理的操作结果,包括指定图像URL的操作和content)和- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock;(查询普通缓存进程)这个函数中才开始真正下载此URL中的内容。

先来看第一个,返回SDWebImageOptionsResult:

这里先将任务加入到正在执行的列表里面,然后再对context进行预处理,源代码是没有对options进行说明处理的,然后将context跟options放入result里面。context的处理源代码就不贴出来了,大概就是对SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilter、SDWebImageContextCacheSerializer这3个进行一个判断,看是否有自定义的传过来,没有就用默认的。

- (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context {
    SDWebImageOptionsResult *result;
    SDWebImageMutableContext *mutableContext = [SDWebImageMutableContext dictionary];
    
    // Image Transformer from manager
    // 来自管理器的图像转换器
    if (!context[SDWebImageContextImageTransformer]) {
        id<SDImageTransformer> transformer = self.transformer;
        [mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer];
    }
    // Cache key filter from manager
    // 从管理器缓存密钥过滤器
    if (!context[SDWebImageContextCacheKeyFilter]) {
        id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
        [mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter];
    }
    // Cache serializer from manager
    // 从管理器缓存序列化器
    if (!context[SDWebImageContextCacheSerializer]) {
        id<SDWebImageCacheSerializer> cacheSerializer = self.cacheSerializer;
        [mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer];
    }
    
    if (mutableContext.count > 0) {
        if (context) {
            [mutableContext addEntriesFromDictionary:context];
        }
        context = [mutableContext copy];
    }
    
    // Apply options processor
    // 应用操作处理器
    if (self.optionsProcessor) {
        result = [self.optionsProcessor processedResultForURL:url options:options context:context];
    }
    if (!result) {
        // Use default options result
        // 使用默认操作结果
        result = [[SDWebImageOptionsResult alloc] initWithOptions:options context:context];
    }
    
    return result;
}
第二个callCacheProcessForOperation的调用

这里主要是判断要到哪里去取数据,ImageCache,还是去下载,接下来就进入这个方法看一下。

这里主要是判断任务是否该走缓存查询,或者直接下载。如果是缓存查询,就进入SDImageCache里面进行缓存查询,且在此处理缓存结果的回调。否则就调用callDownloadProcessForOperation进入下一步判断。
①. 拿到imageCache,拿到缓存类型queryCacheType
②. 通过 options判断,走缓存还是下载。如果走缓存,则调用SDImageCache里面的queryImageForKey(开始进入SDImageCache的逻辑);如果走下载,则调用callDownloadProcessForOperation开始下载前的一些处理。

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Grab the image cache to use
    // 获取要使用的图像缓存
    id<SDImageCache> imageCache;
    //判断SDWebImageContextImageCache对应内容是否符合协议SDImageCache
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    
    // Get the query cache type
    // 获取查询缓存类型,存在的话就将其转换为integer类型数据
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
    
    // Check whether we should query cache
    // 检查是否需要查询缓存
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    //需要查询缓存
    if (shouldQueryCache) {
        //将url和context结合起来转换成一个cache的key
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);//定义一个弱引用self的变量,用于下面的block,避免循环引用
        //开始查询
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);//定义一个强引用self的变量
            //如果operation存在并且operation被取消了
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                // 用户取消图像组合操作,调用CompletionBlock块,做出相应提示,说明用户取消了图像组合操作
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                //将该operation安全移除进程,然后结束函数
                [self safelyRemoveOperationFromRunning:operation];
                return;
            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
                //SDWebImageContextImageTransformer对应内容存在并且cachedImage不存在
                // 有机会查询原始缓存而不是下载,在原始缓存中查找
                // Have a chance to query original cache instead of downloading
                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                return;
            }
            
            // Continue download process
            // 继续下载过程,这里的下载不同于下面的下载,这里的下载是从缓存中下载的,而不是url中
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        //不需要查询缓存
        // 继续下载过程,这里的下载是直接从url中下载的
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}
在这里调用了下载方法callDownloadProcessForOperation

来看看:

- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Grab the image loader to use
    // 获取要使用的图像加载器
    id<SDImageLoader> imageLoader;
    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
        imageLoader = context[SDWebImageContextImageLoader];
    } else {
        imageLoader = self.imageLoader;
    }
    
    // Check whether we should download image from network
    // 检查是否需要从网络下载图像
    BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
    //(没有缓存图片) || (即使有缓存图片,也需要更新缓存图片)
    shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
    //(代理没有响应imageManager:shouldDownloadImageForURL:消息,默认返回yes,需要下载图片)|| (imageManager:shouldDownloadImageForURL:返回yes,需要下载图片)
    shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
    shouldDownload &= [imageLoader canRequestImageForURL:url];
    //如果应该从url下载图像
    if (shouldDownload) {
        //存在缓存图片 && 即使有缓存图片也要下载更新图片
        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.
            // 如果在缓存中找到了图像,但是sdwebimagerfreshcached被提供了,通知缓存的图像
            // 并尝试重新下载它,以便NSURLCache有机会从服务器刷新它。
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
            // 将缓存的图像传递给图像加载程序。图像加载程序应该检查远程图像是否等于缓存的图像。
            SDWebImageMutableContext *mutableContext;
            if (context) {
                mutableContext = [context mutableCopy];
            } else {
                mutableContext = [NSMutableDictionary dictionary];
            }
            mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
            context = [mutableContext copy];
        }
        
        @weakify(operation);
        // ========== operation.loaderOperation ========== //下载操作
        operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @strongify(operation);
            //如果操作不存在并且任务被取消,则什么都不做,避免和其他的completedBlock重复
            if (!operation || operation.isCancelled) {
                // 用户取消图像组合操作
                // Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url];
                //存在缓存图片 && 即使有缓存图片也要下载更新图片 && 错误域相同 && error的代码为远程位置指定缓存的映像不被修改
            } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                // Image refresh hit the NSURLCache cache, do not call the completion block
                // 图像刷新击中NSURLCache缓存,不调用完成块
                //错误域相同 && 镜像加载操作在完成之前取消,在异步磁盘缓存查询期间,或在实际网络请求之前等待
            } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
                // Download operation cancelled by user before sending the request, don't block failed URL
                // 下载操作被用户在发送请求前取消,不要阻止失败的URL
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                //如果错误存在
            } else if (error) {
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                //是否应该添加该url到错误名单中
                BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];
                //在错误url名单中添加当前的url
                if (shouldBlockFailedURL) {
                    SD_LOCK(self.failedURLsLock);
                    [self.failedURLs addObject:url];
                    SD_UNLOCK(self.failedURLsLock);
                }
                //下载成功
            } else {
                //如果需要下载失败后重新下载,则将当前url从失败url名单里移除
                if ((options & SDWebImageRetryFailed)) {
                    SD_LOCK(self.failedURLsLock);
                    [self.failedURLs removeObject:url];
                    SD_UNLOCK(self.failedURLsLock);
                }
                // Continue store cache process
                // 继续存储缓存进程
                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }
            //如果完成,从当前运行的操作列表里移除当前操作
            if (finished) {
                [self safelyRemoveOperationFromRunning:operation];
            }
        }];
        // ========== operation.loaderOperation ========== //
        //存在缓存图片
    } else if (cachedImage) {
        //调用完成的block
        [self callCompletionBlockForOperation:operation 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
        // 调用完成的block
        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
        //删去当前的下载操作
        [self safelyRemoveOperationFromRunning:operation];
    }
}

下面来看看两个类的核心方法:

SDImageCache

主要用来查看缓存

 // ==============  SDImageCache.m ============== //
@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache;//内存缓存
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;//磁盘缓存路径
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;//ioQueue唯一子线程;
 (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    //key不存在,结束查找函数
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    // Invalid cache type
    // 无效的缓存类型,结束查找函数
    if (queryCacheType == SDImageCacheTypeNone) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // First check the in-memory cache...
    //================查看内存的缓存=================//
    UIImage *image;
    //如果查询缓存类型不是只处理磁盘缓存,
    if (queryCacheType != SDImageCacheTypeDisk) {
        //在内存中找
        image = [self imageFromMemoryCacheForKey:key];
    }
    
    // 如果存在,直接调用block,将image,data,CaheType传进去
    if (image) {
        //如果操作为解码获取第一帧产生静态图像
        if (options & SDImageCacheDecodeFirstFrameOnly) {
            // Ensure static image
            // 确保静态图像
            Class animatedImageClass = image.class;
            //如果图像是动画 || animatedImageClass是UIImage的实例 && animatedImageClass遵循SDAnimatedImage协议
            if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#if SD_MAC
                image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
                image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
            }
        //操作为确保总是与提供的类产生图像
        } else if (options & SDImageCacheMatchAnimatedImageClass) {
            // Check image class matching
            // 检查图像类匹配
            Class animatedImageClass = image.class;
            Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
            if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
                image = nil;
            }
        }
    }

    //应该只查找内存
    BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        //因为图片有缓存可供使用,所以不用实例化NSOperation,直接返回nil
        return nil;
    }
    
    // Second check the disk cache...
    //================查看磁盘的缓存=================//
    NSOperation *operation = [NSOperation new];
    // Check whether we need to synchronously query disk
    // 1. in-memory cache hit & memoryDataSync
    // 2. in-memory cache miss & diskDataSync
    // 检查是否需要同步查询磁盘
    //(在内存缓存命中) && memoryDataSync
    //(内存缓存错过) && diskDataSync
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
    void(^queryDiskBlock)(void) =  ^{
        // 在用之前就判断operation是否被取消了,作者考虑的非常严谨,如果取消了就直接结束该函数了
        if (operation.isCancelled) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return;
        }
        
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            if (image) {
                // 图像来自内存缓存,但需要图像数据
                // the image is from in-memory cache, but need image data
                diskImage = image;
            } else if (diskData) {
                //应该缓存到内存
                BOOL shouldCacheToMomery = YES;
                if (context[SDWebImageContextStoreCacheType]) {
                    SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
                    shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
                }
                // 解码图像数据,只有在内存缓存错过
                // decode image data only if in-memory cache missed
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
                    // cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
                    NSUInteger cost = diskImage.sd_memoryCost;
                    //存入内存缓存中
                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            if (doneBlock) {
                if (shouldQueryDiskSync) {
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                    });
                }
            }
        }
    };
    
    // Query in ioQueue to keep IO-safe
    // 在唯一的子线程:self.ioQueue中查询,保证io安全
        //应该同步查询磁盘
    if (shouldQueryDiskSync) {
        //线程等待
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        //线程不等待
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

SDWebImageDownloader

核心方法:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // 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.
    // URL将被用作回调字典的键,所以它不能为nil。如果为nil,立即调用没有图像或数据的已完成块。
    if (url == nil) {
        //如果completedBlock存在但是因为没有url所以就没办法访问,就直接提示错误就行
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
            completedBlock(nil, nil, error, YES);
        }
        return nil;
    }
    
    SD_LOCK(self.operationsLock);
    //定义一个下载操作取消令牌
    id downloadOperationCancelToken;
    //当前下载操作中取出SDWebImageDownloaderOperation实例
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    // 有一种情况是,操作可能被标记为完成或取消,但没有从'self.URLOperations'中删除。
    //操作不存在||操作已完成||操作被取消
    if (!operation || operation.isFinished || operation.isCancelled) {
        //创建一个下载操作给operation
        //operation中保存了progressBlock和completedBlock
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        //如果操作不存在,即上述创建失败,返回一个错误信息
        if (!operation) {
            SD_UNLOCK(self.operationsLock);
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                completedBlock(nil, nil, error, YES);
            }
            return nil;
        }
        @weakify(self);
        //如果创建成功了,我们再继续完善其completionBlock代码块
        operation.completionBlock = ^{
            @strongify(self);
            if (!self) {
                return;
            }
            //在操作数组中删除此url的操作
            SD_LOCK(self.operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self.operationsLock);
        };
        //将该url的下载操作赋值给self.URLOperations操作数组
        self.URLOperations[url] = operation;
        // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
        // 在提交到操作队列之前添加处理程序,避免操作在设置处理程序之前完成的竞态条件。
        //下载操作取消令牌
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        // 根据苹果文档完成所有配置后,才能将操作添加到操作队列中。
        // 'addOperation:'不会同步执行'operation.completionBlock',因此不会导致死锁。
        //将此下载操作添加到下载队列
        [self.downloadQueue addOperation:operation];
    } else {
        // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
        // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
        // 当我们重用下载操作来附加更多的回调时,可能会有线程安全问题,因为回调的getter可能在另一个队列(解码队列或委托队列)
        // 所以我们锁定了这里的操作,在'SDWebImageDownloaderOperation'中,我们使用' @synchronized(self)'来确保这两个类之间的线程安全。

        @synchronized (operation) {
            //下载操作取消令牌
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        }
        //如果该操作没有在执行
        if (!operation.isExecuting) {
            //给该操作赋相应的优先级操作
            if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            } else {
                operation.queuePriority = NSOperationQueuePriorityNormal;
            }
        }
    }
    SD_UNLOCK(self.operationsLock);
    
    //创建该下载的token,这里 downloadOperationCancelToken 默认是一个字典,存放 progressBlock 和 completedBlock
    //使用operation初始化该下载操作token
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url; //保存url
    token.request = operation.request; //保存请求
    token.downloadOperationCancelToken = downloadOperationCancelToken; //保存下载操作取消令牌
    
    return token;
}

我们发现这个方法在之前的方法里并没有调用过,我们在全局搜索下这个函数在哪用过呢?
在这个函数里:
这个方法是一个工具,用来生成SDWebImageDownloaderOptions对象,我们向前找发现在SDWebImageManager的callDownloadProcessForOperation方法里调了这个:

- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock {
    UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage];
    
    SDWebImageDownloaderOptions downloaderOptions = 0;
    if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
    if (options & SDWebImageProgressiveLoad) downloaderOptions |= SDWebImageDownloaderProgressiveLoad;
    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 (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
    if (options & SDWebImageMatchAnimatedImageClass) downloaderOptions |= SDWebImageDownloaderMatchAnimatedImageClass;
    
    if (cachedImage && options & SDWebImageRefreshCached) {
        // force progressive off if image already cached but forced refreshing
        downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad;
        // ignore image read from NSURLCache if image if cached but force refreshing
        downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
    }
    
    return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
}

然后我们再看downloadImageWithURL里用到的的方法,这个才是真正的下载:
通过这个函数createDownloaderOperationWithUrl赋值给了operation然后通过operation才赋值给了token

- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options
                                                                                  context:(nullable SDWebImageContext *)context {
    //创建一个等待时间
    NSTimeInterval timeoutInterval = self.config.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
    // 为了防止潜在的重复缓存(NSURLCache + SDImageCache),我们禁用图像请求的缓存
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
    //创建下载请求
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
    mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
    mutableRequest.HTTPShouldUsePipelining = YES;
    //线程安全的创建一个请求头
    SD_LOCK(self.HTTPHeadersLock);
    mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
    SD_UNLOCK(self.HTTPHeadersLock);
    
    // Context Option
    // Context选项
    SDWebImageMutableContext *mutableContext;
    if (context) {
        mutableContext = [context mutableCopy];
    } else {
        mutableContext = [NSMutableDictionary dictionary];
    }
    
    // Request Modifier
    //请求修饰符,设置请求修饰符,在图像加载之前修改原始的下载请求。返回nil将取消下载请求。
    id<SDWebImageDownloaderRequestModifier> requestModifier;
    if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
        requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
    } else {
        //self.requestModifier默认为nil,表示不修改原始下载请求。
        requestModifier = self.requestModifier;
    }
    
    NSURLRequest *request;
    //如果请求修饰符存在
    if (requestModifier) {
        NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
        // If modified request is nil, early return
        // 如果修改请求为nil,则提前返回
        if (!modifiedRequest) {
            return nil;
        } else {
            request = [modifiedRequest copy];
        }
    } else {
        request = [mutableRequest copy];
    }
    // Response Modifier
    // 响应修饰符,设置响应修饰符来修改图像加载期间的原始下载响应。返回nil将标志当前下载已取消。
    id<SDWebImageDownloaderResponseModifier> responseModifier;
    if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
        responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
    } else {
        //self.responseModifier默认为nil,表示不修改原始下载响应。
        responseModifier = self.responseModifier;
    }
    //如果响应修饰存在
    if (responseModifier) {
        mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
    }
    // Decryptor
    // 图像解码,设置解密器对原始下载数据进行解密后再进行图像解码。返回nil将标志下载失败。
    id<SDWebImageDownloaderDecryptor> decryptor;
    if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
        decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
    } else {
        //self.decryptor默认为nil,表示不修改原始下载数据。
        decryptor = self.decryptor;
    }
    //如果图像解码操作存在
    if (decryptor) {
        mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
    }
    
    context = [mutableContext copy];
    
    // Operation Class
    // 操作类
    Class operationClass = self.config.operationClass;
    //操作类存在 && 操作类是NSOperation的实例类 && 操作类遵守SDWebImageDownloaderOperation协议
    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
        // Custom operation class
        // 自定义操作类(可以自行修改和定义)
    } else {
        //默认操作类
        operationClass = [SDWebImageDownloaderOperation class];
    }
    //创建下载操作:SDWebImageDownloaderOperation用于请求网络资源的操作,它是一个 NSOperation 的子类
    NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
    
    //如果operation实现了setCredential:方法
    if ([operation respondsToSelector:@selector(setCredential:)]) {
        //url证书
        if (self.config.urlCredential) {
            operation.credential = self.config.urlCredential;
        } else if (self.config.username && self.config.password) {
            operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
        }
    }
        
    //如果operation实现了setMinimumProgressInterval:方法
    if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
        operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
    }
    
    //设置该url的操作优先级
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    
    if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
        // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
        // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
        // 通过系统地模拟后进先出的执行顺序,前一个添加的操作可以依赖于新操作
        // 这样可以保证先执行新操作,即使有些操作完成了,同时又追加了新操作
        // 仅仅使上次添加的操作依赖于新的操作并不能解决这个问题。参见测试用例#test15DownloaderLIFOExecutionOrder

        for (NSOperation *pendingOperation in self.downloadQueue.operations) {
            [pendingOperation addDependency:operation];
        }
    }
    
    return operation;
}

SDWebImage总共嵌套了七八层类去完成操作,比之前学的JSONModel复杂很多,我看得还比较浅,在这补充下一些类的作用:

  • SDWebImageDownloaderOperation: 下载器的具体实现,继承自NSOperation,使用NSURLSession来执行具体的下载操作。
  • SDWebImageDownloaderConfig: 下载器的配置类,用于配置下载选项,例如是否使用NSURLCache,是否允许蜂窝网络下载等。
  • SDWebImageManager: 负责图片下载和缓存的管理。
  • SDImageCache: 图片缓存器,用于缓存已下载的图片数据,避免重复下载。
  • SDImageCacheConfig: 缓存器的配置类,用于配置缓存选项,例如缓存路径、缓存时长等。
  • SDWebImageOperation: 图片加载器的具体实现,继承自NSOperation,用于加载和显示图片。
  • SDAnimatedImage: 用于播放动态图片。
  • SDAnimatedImageView: 用于显示动态图片的UIImageView子类。
  • SDWebImageCompat: 用于提供一些跨平台的API,例如判断图片类型、生成缩略图等。
  • UIImage+MultiFormat: 用于支持多种图片格式,例如WebP、GIF等。
  • SDWebImageDefine: 定义了一些宏,例如SD_LOCK、SD_UNLOCK等。
  • SDWebImageError: 定义了一些错误码和错误描述。
  • SDWebImagePrefetcher: 用于预下载图片数据,避免在需要显示图片时还需要进行下载操作。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【iOS】—— SDWebImage源码学习 的相关文章

  • 如何在Mac的cocoa应用程序中允许用户的主文件夹的权限

    我有一个 Mac 应用程序 其中我正在从用户的主目录中删除一些数据 我的应用程序被拒绝 原因如下 The app only finds files in the Downloads folder It would be appropriat
  • viewDidLoad 中的帧大小错误[重复]

    这个问题在这里已经有答案了 可能的重复 为什么我必须在 viewDidLoad 中手动设置视图的框架 https stackoverflow com questions 6757018 why am i having to manually
  • UITableView 顶部出现间隙[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我不确定现在问这个问题是否合适 我正在表视图上使用 Xcode 5 预览版 现在的问题是我的表格视图是否被选择为group比我在第一个单元
  • UIScrollView 滚动时捕捉到位置

    我正在尝试实现一个捕捉到点的滚动视图滚动时 我在这里看到的所有帖子都是关于在用户结束拖动滚动条 之后 捕捉到某个点的 我想让它在拖动过程中折断 到目前为止 我已经用它来停止拖动后的惯性 并且效果很好 func scrollViewWillE
  • XCTestCase:ld:未找到架构 x86_64 的符号

    我只是想实现我的第一个 XCTestCase 实现 一切都构建良好 但是当我执行测试用例时 我面临 构建失败 Undefined symbols for architecture x86 64 OBJC CLASS Node referen
  • 支持从右到左的纯布局

    iOS纯布局支持从右到左的语言吗 我们如何在阿拉伯语言的代码中实现它 而不需要去设置并选择区域和格式语言 Thanks 您可以通过在 Xcode gt 编辑方案 gt 运行 gt 选项 选项卡 gt 应用程序语言 gt 从右到左伪语言中选择
  • 检查 Objective-C 中从 JSON 字符串返回的空值

    我有一个JSON来自网络服务器的对象 日志是这样的 status success UserID 15 Name John DisplayName John Surname Smith Email email Telephone null F
  • 在视频录制中,ios 中的 sdk 导航不会录制音频

    我尝试使用 HERE SDK 通过逐向导航来录制视频 当导航未启动时 视频录制工作正常 但当导航正在进行并尝试录制视频时 会出现此错误 avas AVAudioSession mm 1074 AVAudioSession setActive
  • UIPageControl 未显示

    我使用以下内容来显示滚动视图和页面控制 scrollView UIScrollView alloc initWithFrame CGRectMake 0 0 320 179 pageControl UIPageControl alloc i
  • iOS AVPlayer 在播放图标上显示带有十字线的屏幕

    Screen Shot of AVPlayer error https i stack imgur com rX1AT jpg 下面是代码片段 url是视频url 此错误是随机发生的 我无法追踪问题所在 此外 视频还存储在云端和视频播放器中
  • OCMock - 尽管是预期的,但仍调用了意外的方法

    这是经过测试的代码 if MFMailComposeViewController canSendMail MFMailComposeViewController mailComposeController MFMailComposeView
  • 使用 MPVolumeView 滑块调整音量时隐藏设备音量 HUD 视图

    我在 iPad 应用程序中使用 MPMoviePlayer 实现视频播放器 使用 MPVolumeView 进行音量控制 我的问题是 当我滚动音量视图来调整音量时 会显示设备的音量平视显示器覆盖 如下面的屏幕截图所示 如何禁用此系统音量指示
  • UIModalPresentationFormSheet 的圆角

    如果这是一个明显的问题 请原谅我 我是个相对较新的人 我有一个模态视图 我设置了自定义尺寸和圆角 void viewWillLayoutSubviews super viewWillLayoutSubviews self view supe
  • 如何在 iOS8 上以编程方式创建一个没有 Storyboard 的 Today 小部件?

    我尝试删除故事板文件和相关的Info plist进入但这次扩展停止工作 它甚至不能从 XCode 启动 The operation couldn t be completed LaunchServicesError error 0 在常规应
  • 使用 strftime 将 NSDate 转换为 String

    如何将 NSDate 转换为使用 strftime 说明符格式化的 NSString 你可以使用 strftime NSDate date NSDate date time t time date timeIntervalSince1970
  • 改变 RGB 颜色的色调

    我正在尝试编写一个函数来改变 RGB 颜色的色调 具体来说 我在 iOS 应用程序中使用它 但数学是通用的 下图显示了 R G 和 B 值如何随色调变化 看起来 编写一个函数来改变色调似乎应该是一个相对简单的事情 而不需要对不同的颜色格式进
  • 为什么Android和IOS11无法通过NFC通信

    目前正在使用 React Native 并尝试使用反应本机 NFC ios https www npmjs com package react native nfc ios and 反应本机 NFC https github com Nov
  • 如何通过 CollectionView 中的流布局将单元格对齐到顶部

    在此代码中 我尝试更改 UICollectionView 的第一个单元格的大小以及具有相同大小的其他单元格的大小 但在第一行中 当我想要两个单元格出现时 只有一个单元格出现 func collectionView collectionVie
  • 如何在核心数据中应用group by子句

    我正在实现 tableview 我想在 tableview 部分显示类名 并且我正在尝试获取 使用核心数据实现的数据库中的类值 我想使用类名上的 group by 子句获取数据 这是我的代码 AppDelegate appDelegate
  • Windows 下 iOS 开发的替代方案 [重复]

    这个问题在这里已经有答案了 可能的重复 如何使用 Windows 开发机为 iPhone 进行开发 https stackoverflow com questions 22358 how can i develop for iphone u

随机推荐

  • Postman测试webService接口

    1 打开Postman界面如下 设置Content Type text xml 2 设置body 3 请求结果如下 4 至此通过Postman进行webService接口测试测试完毕
  • 使用过滤器和request装饰增强来彻底解决乱码问题

    在客户端以get或者post方式提交数据时 可能会出现客户端与服务端编码集不匹配而发生乱码的现象 在对post方式产生的乱码我们可以用 request setCharacterEncoding utf 8 语句来解决 在对get方式产生的乱
  • maven&​ Plugin ‘org.apache.tomcat.maven:tomcat7-maven-plugin:2.2’ not found​报错解决【问题及解决过程记录】

    目录 什么是 Maven 安装 解压后需要配置环境变量 在path新增路径 验证maven安装成功 Win R打开cmd 输入mvn v 在配置文件中设置本地仓库路径 maven仓库的种类和关系 编辑 maven目录结构 编辑 maven的
  • oracal从入门到精通(一)

    1 1了解最新版本Oracle 11g 可以在Oracle的官方网站www oracle com获取Oracle的版本信息 本书中要讲解的是Oracle 11g的第1版 所以在这里只对Oracle 11g的各版本做以说明 Oracle 11
  • 关于Vue.js组件巨详细的一篇文章

    文章目录 Vue js 组件 全局注册 组件基础 组件命名规则 template 选项 data 选项 局部注册 组件通信 父组件向子组件传值 props 命名规则 单项数据流 props 类型 props 验证 非 props 属性 子组
  • Iterative Shrinkage Thresholding Algorithm

    Iterative Shrinkage Thresholding Algorithm ISTA ISTA 对于一个基本的线性逆问题 y A x
  • 【Webpack5】从零开始学习基础配置

    基本使用 Webpack 是一个静态资源打包工具 它会以一个或多个文件作为打包的入口 将我们整个项目所有文件编译组合成一个或多个文件输出出去 输出的文件就是编译好的文件 就可以在浏览器段运行了 我们将 Webpack 输出的文件叫做 bun
  • [转帖]国产操作系统概念及历史,目前国产操作系统有哪些?

    国产操作系统概念及历史 目前国产操作系统有哪些 2018 05 19 21 20 18 作者 ywnz 稿源 云网牛站整理 https ywnz com linuxgcxt 1786 html 以下为你讲解国产操作系统概念 国产操作系统历史
  • 【链块观点01期】当区块链的风刮到教育行业,哪些变革是注定要发生的?

    原文链接 区块链应用案例 当区块链的风刮到教育行业 哪些变革是注定要发生的 一 教育是国之大计 古往今来 教育都在人类生活中占有着极其重要的位置 教育与社会的进步 文明的发展程度紧密相关 中国正在努力建设和谐社会 而如何达成社会主义和谐社会
  • Android Studio运行时自带签名配置过程详解

    文章转载自 http teachcourse cn 2053 html 摘要 Android Studio比Eclipse在配置Project签名文件时方便很多 可以同时指定debug和release两种模式下的签名文件 debug模式即A
  • SpringBoot的自动装配

    一 什么是SpringBoot自动装配 SpringBoot 定义了一套接口规范 这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的META INF spring factories文件 将文件中配置的类型信息加载到
  • Qt 交叉编译

    背景 本来Qt源码是可以直接使用交叉编译器编译通过 也可以使用编译通过的qmake生成可执行程序 但是可执行程序在没有Qt的环境下无法正常运行 因此我们不只是仅需要编译源码 还需要将依赖的环境编译出来 这样打包可执行程序的同时将相应的依赖拷
  • 爬虫项目二十一:需要简历吗?用Python轻松爬下上千份简历模板

    文章目录 前言 一 获取详情页url 二 获取下载链接 三 保存模板 前言 用Python对站长之家的上千个简历模板批量爬取保存 提示 以下是本篇文章正文内容 下面案例可供参考 一 获取详情页url 我们进入站长之家的免费模板网站 http
  • linux修改rc.local权限,CentOS7中rc.local中的指令不能生效问题。

    问题 在系统中 etc rc local设置自启动命令的时候 重启计算机 文件中的指令无法生效 我这里编译安装了一个mongodb数据库的服务 然后修改了环境变量 写入到了 bash profile 文件中 在shell中可以直接执行命令
  • idea创建python项目

    windows安装pyhon Window 安装 Python 哭哭啼的博客 CSDN博客下载官网地址 https www python org downloads windows 此处下载最新版本 64位 安装找到下载的文件解压文件即可配
  • 动态链接库和静态链接库的原理及区别

    静态连接库就是把 lib 文件中用到的函数代码直接链接进目标程序 程序运行的时候不再需要其它的库文件 动态链接就是把调用的函数所在文件模块 DLL 和调用函数在文件中的位置等信息链接进目标程序 程序运行的时候再从DLL中寻找相应函数代码 因
  • Ubuntu 下同局域网主机访问Tomcat 服务器

    转自 https blog csdn net zm yang article details 70483439 搭建Tomcat环境 自己写些小应用 需要用到服务器 便在Ubuntu环境下搭建了个Tomcat服务器 搭建方法很简单 去官网下
  • 数据转换之数据清洗

    一 实现对文本文件personnel data txt中的数据进行数据粒度的转换 即将文本文件personnel data txt中字段为household register的数据统一成省份 直辖市 并输出到文本文件personnel da
  • 大数据时代移动边缘计算架构中的差分隐私保护(二)

    大数据时代移动边缘计算架构中的差分隐私保护 二 实际上 给数据加拉普拉斯噪声或者是指数噪声是针对中心式的差分隐私处理框架的 对于本地化的差分隐私处理框架 现在已有的是采用随即相应技术 Bloom Filter等技术满足 本地化差分隐私 LD
  • 【iOS】—— SDWebImage源码学习

    SDWebImage源码 文章目录 SDWebImage源码 1 UIKit层 sd internalSetImageWithURL 1 取消之前下载操作 2 设置占位图 3 判断URL是否合法 2 工具层 SDWebImageManage