美文网首页iOS干货
SDWebImage源码学习

SDWebImage源码学习

作者: B_C_H | 来源:发表于2017-12-18 16:44 被阅读4次

    前言

    因为对大神的开源代码非常崇拜,所以开始学习这些开源的代码。
    这是一年前学习源码时写在印象笔记里的笔记,过了一年,今天又把最新的SDWebImage下下来看,发现没什么大变化。

    分析

    1.我们平时开发,用的最多的就是:
    - (void)sd_setImageWithURL:(nullable NSURL *)url 
              placeholderImage:(nullable UIImage *)placeholder
    

    这一系列方法.使用非常简单:

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://example.com/image.jpg"]
                          placeholderImage:[UIImage imageNamed:@"placeholder"]];
    
    2.那这个方法是怎么实现的呢?
    a.这个方法在UIImageView+WebCache分类中, 有一系列.当最终这些方法都会调用下面这个.区别只是传的参数,要么传了空,要么是默认值.
    
    /**
     <# 平时用的方法,只是对这个方法包装了一下 #>
    
     @param url            <# 网络图片链接 #>
     @param placeholder    <# 占位图 #>
     @param options        <# 一个特殊的标记 #>
     @param progressBlock  <# 进度回调block #>
     @param completedBlock <# 网络图像加载完成回调的block #>
     */
         - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                       options:(SDWebImageOptions)options
                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                     completed:(nullable SDExternalCompletionBlock)completedBlock;
               
    

    b.上面这个方法又调用了UIView+WebCache分类的方法:

    /**
     <# 根据指定的url,异步下载图像,并缓存.设置imageView中的image#> 
     @param url            <#url 指定的url#>
     @param placeholder    <#placeholder 展占图#>
     @param options        <#options 一个特殊的标记#>
     @param operationKey   <#operationKey 下载操作(operation)的key值,默认为类名#>
     @param setImageBlock  <#setImageBlock 自定义设置图像的代码 #>
     @param progressBlock  <#progressBlock 下载过程中进度回调的block #>
     @param completedBlock <#completedBlock 下载完成回调的block #>
     */
               - (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;
    
    3.现在对这个方法的代码一行行分析:
    3.1
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    

    a、数据校验,如果设置了operation的key就使用设置的,默认使用类名作为key
    b、根据operationKey找到对应的操作,从下载队列取消正在下载的操作
    c、使当前 UIImageView 中的所有操作都被 cancel. 不会影响之后进行的下载操作.
    d、这里只是说了,不取消,会影响后面的下载操作.那为什么会影响?影响的结果是什么?

    为什么会影响:

    因为UIImageView一个对象应该只对应的是一个下载操作,如果UIImageView的当前对象,他的前一个下载操作未完成,然后又来了一个下载操作(同一个imageView多次调用了这个方法),这样就有了2个下载操作了.这不符合一个UIImageView对象对应一个下载操作.

    影响的结果:

    当前一个下载操作将图像请求完成,展示到imageView上,过一会后面的操作也请求完成,这样又会再一次设置imageview展示的图像.虽然这可能不会有太大的影响,但是至少是消耗性能了,做了没必要的操作.

    3.2 利用runtime,对url做一次retain
         objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    3.3
    if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
            });
        }
    

    a.如果传入的 options 中没有 SDWebImageDelayPlaceholder(默认情况下 options == 0),
    b.那么就会为 UIImageView 添加一个临时的 image, 也就是占位图.
    c.SDWebImageDelayPlaceholder(我的理解就是,不让用占位图),取反就是允许使用占位图

    3.4
    //检查url不为空才去加载
         if (url) {
         }
    //否则直接调用加载完成回调的block,并吧错误传入block
         else{
         } 
    //检查activityView(指示器)是否可用
    // check if activityView is enabled or not
         if ([self sd_showActivityIndicatorView])               
              [selfsd_addActivityIndicator];
         }
    
    3.5调用 SDWebImageManager的对象方法去获取图像
    /**
     <# 指定的url如果没有缓冲,就去下载图像 #>
    
     @param url            <#url 指定的url#>
     @param options        <#options 一个特殊的标记#>
     @param progressBlock  <#progressBlock 下载过程中进度回调的block#>
     @param completedBlock <#completedBlock 下载完成回调的block#>
    
     @return <#return value 下载操作#>
     */
         - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(nullable SDInternalCompletionBlock)completedBlock;
    
    //在这个方法中,首先会验证url的有效性
         if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        } 
        // Prevents app crashing on argument type error like sending NSNull instead of NSURL
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    
    3.6创建了一个包装了一个看着像下载操作的却并不是下载操作的类SDWebImageCombinedOperation,

    这个类本身不是操作类,只是他内部有一个NSOperation属性(注意:在这里,operation属性还没有被赋值,是nil).当然这个类还遵循了一个协议,SDWebImageOperation.这个协议只有一个取消的方法-cancel.

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
    
    3.7去黑名单匹配当前url是否是下载失败过的url
    BOOL isFailedUrl = NO;
        if (url) {
            @synchronized (self.failedURLs) {
                isFailedUrl = [self.failedURLs containsObject:url];
            }
        }
    
    3.8如果url验证不通过,直接调用加载完成的回调block,并且传error出去
    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;
        }
    
    3.9准备下载,首先将下载操作添加到数组保存起来
     @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }
    
    3.10根据url获取到缓存的key
     NSString *key = [self cacheKeyForURL:url];
    
    //在正真去网上下载数据之前,调用SDImageCache的对象方法,
    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
    
    3.11在这个方法中,首先会验证缓存的key的有效性.无效,说明缓存中没有找到对应的图像;调用block去下载;
    if (!key) {
            if (doneBlock) {              //标记好无缓存
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return nil;
        }
    
    3.12查询内存缓存中是否能找到对应的image,如果找到了,将image放doneBlock中传出去,如果没找到,继续往后走
    UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            NSData *diskData = nil;
            if ([image isGIF]) {
                diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            }
            if (doneBlock) {
                doneBlock(image, diskData, SDImageCacheTypeMemory);
            }
            return nil;
        }
    
    3.13然后再在磁盘缓存中查找,是否有对应的image,在这,不管找没找到,都将获取diskImage(没找到时为nil)放doneBlock中传出去.最后返回operation.
       NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                UIImage *diskImage = [self diskImageForKey:key];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
    
                if (doneBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                    });
                }
    
    3.14现在开始分析doneBlock中做写什么.当从内存缓存,磁盘缓存查找回来,就开始执行这个block
    typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
    

    在这个block中,首先判断刚才创建的operation是否已经取消

     if (operation.isCancelled) {
             //将已经取消的操作,从正在执行的操作数组中移除 
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
    
    //如果(
              (`图片不存在` || `options包含SDWebImageRefreshCached(刷新缓存)`)
              && 不能响应imageManager:shouldDownloadImageForURL:方法)
          ||(或者)  
                       (imageManager:shouldDownloadImageForURL:返回值为YES( 没有缓存过的图片将不会下载))
          if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
          
          //如果在缓存中找到了image,但是需要刷新缓存,尝试重新下载
          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];
                }
    
    3.16 如果图片不存在,或者设置了SDWebImageRefreshCached 需要刷新缓存,就在下载
     // download if no image or requested to refresh anyway, and download allowed by delegate
    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;
                }
    
    3.17调用 SDWebImageDownloader(下载器)的对象方法开始从网络下载图片:
     /**
     <# 根据url创建一个异步下载器实例,当下载完成或下载失败报错时会通知代理 #>
     @param url            <#url 指定的url#>
     @param options        <#options 标记 #>
     @param progressBlock  <# 进度回调block #>
     @param completedBlock <# 网络图像加载完成回调的block #>
     @return <#return value description#>
     */
    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
    
    3.18然后在上面的方法中,会立即调用另外一个方法并返回
    /**
     <#Description#>
     @param progressBlock  <#progressBlock 进度回调的block #>
     @param completedBlock <#completedBlock 加载完毕回调的block #>
     @param url            <#url 指定图像的url #>
     @param createCallback <#createCallback 创建请求的block #>
     @return <#return value description#>
     */
         - (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                               completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                       forURL:(nullable NSURL *)url
                                               createCallback:(SDWebImageDownloaderOperation *(^)())createCallback
    
    3.19在这个方法中,首先验证url不能为nil,因为后面保存callblock(回调的block)需要url作为key
    // 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;
        } 
    
    3.20
    //dispatch_barrier_sync:在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
    dispatch_barrier_sync(self.barrierQueue, ^{
            //根据url作为key去字典中获取对应的operation
            SDWebImageDownloaderOperation *operation = self.URLOperations[url];
            if (!operation) {
                //如果获取的operation不存在,说明是第一次加载,需要回调createCallback,以保证请求被创建好
                operation = createCallback();
                //保存好operation
                self.URLOperations[url] = operation;
    
                __weak SDWebImageDownloaderOperation *woperation = operation;
                operation.completionBlock = ^{
                  //这个block,是在请求执行完成时回调
                  //当请求执行完毕,需要将URLOperations中保存的operation移除
                  SDWebImageDownloaderOperation *soperation = woperation;
                  if (!soperation) return;
                  if (self.URLOperations[url] == soperation) {
                      [self.URLOperations removeObjectForKey:url];
                  };
                };
            }
    
    
        //在这调用operation的对象方法,把progressBlock和completedBlock以字典的形式保存起来,并返回.
        //让downloadOperationCancelToken接收这个字典,所以downloadOperationCancelToken其实是一个可变字典
    
     id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
                     //与每个下载相关的令牌。可以用来取消下载
            token = [SDWebImageDownloadToken new];
            token.url = url;
            token.downloadOperationCancelToken = downloadOperationCancelToken;
        });
    
     然后返回这个token(令牌).
    
     //看到这里,似乎我们根本就没看见请求在哪.其实,前面有提到,当根据url去获取operation为空时,会回调一个createCallback
     //对,就是在createCallback中创建了请求,并开始下载的.
     //下面看看这个block中的代码
    
     //首先会设置超时时间,默认15s
    
      NSTimeInterval timeoutInterval = sself.downloadTimeout;
            if (timeoutInterval == 0.0) {
                timeoutInterval = 15.0;
            }
    
     // 为防止重复缓存(NSURLCache + SDImageCache),如果设置了 SDWebImageDownloaderUseNSURLCache,则禁用 SDImageCache
       这个 request 就用于在之后发送 HTTP 请求.
    
    // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) 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;
            }
    
     //在初始化了这个 request 之后, 又初始化了一个 SDWebImageDownloaderOperation 的实例,
        //这个实例, 就是用于请求网络资源的操作. 它是一个 NSOperation 的子类,
    
     SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
            operation.shouldDecompressImages = sself.shouldDecompressImages;
    
          // 设置 https 访问时身份验证使用的凭据
           if (sself.urlCredential) {
                operation.credential = sself.urlCredential;
            } else if (sself.username && sself.password) {
                operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
            }
    
      //将操作添加到队列
      //只有将它加入到这个下载队列中, 这个操作才会执行.也就是说operation 的 `start` 方法会被执行.开始请求网路下载图像
    
    [sself.downloadQueue addOperation:operation];
     
         //现在开始分析operation 的 `start` 方法里面做了些什么
              //整个下载都是加锁的,安全
               @synchronized (self) {...}
    
         //如果操作已经取消,设置已完成标记,重置操作
          if (self.isCancelled) {
                self.finished = YES;
                [self reset];
                return;
            }
    
     //下面这段代码,最主要的就是,保证程序进入后台,也能保证app中的线程继续工作
     //beginBackgroundTaskWithExpirationHandler只要调用了此函数系统就会允许app的所有线程继续执行,直到任务结束(1,     
    
    [[UIApplicationsharedApplication]backgroundTimeRemaining] 的时间结束 2,调用endBackgroundTask)
          Class UIApplicationClass = NSClassFromString(@"UIApplication");
            BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
            if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
                __weak __typeof__ (self) wself = self;
                UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
                self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                   //如果在系统规定的时间内任务还没有完成,在时间到之前会调用这个block,一般是10分钟
                __strong__typeof(wself) sself = wself;
    
                    if (sself) {
                        [sself cancel];
    
                        [app endBackgroundTask:sself.backgroundTaskId];
                        sself.backgroundTaskId = UIBackgroundTaskInvalid;
                    }
                }];
            }
    
    3.21创建session
    NSURLSession *session = self.unownedSession;
            if (!self.unownedSession) {
                NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
                sessionConfig.timeoutIntervalForRequest = 15;
               
                /**
                 *  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.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                                  delegate:self
                                                             delegateQueue:nil];
                session = self.ownedSession;
            }
           
            self.dataTask = [session dataTaskWithRequest:self.request];
            self.executing = YES;
    
    3.22
    //启动任务,到这里我们用到的UIImageVIew+WebCache里面的方法sd_ImageWithURL:这系列方法是如何达到效果的就分析完毕了
    [self.dataTask resume];
    
    3.23启动任务之后,调用progressBlock,发送开始下载的通知
     for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
                progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
            });
    
    3.24这个方法的操作可以保证新的操作队列不会被旧的影响,同时把该清理的状态都归为完毕
    //任务完成,处理释放对象 
          if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
            UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
            [app endBackgroundTask:self.backgroundTaskId];
            self.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    

    相关文章

      网友评论

        本文标题:SDWebImage源码学习

        本文链接:https://www.haomeiwen.com/subject/kqsmwxtx.html