程序员

SDWebImage 笔记

项目中一直都有使用SDWebImage,对这个框架有一定的了解,但是体系却未能贯通,因此特地整理下,主要参考:

iOS 源代码分析 — SDWebImage

SDWebImage源码剖析(-)

SDWebImage源码剖析(二)

一、简介:

SDWebImage是一个异步下载图片并且支持缓存的UIImageView分类。
主要逻辑为:

  • 查看缓存,如果缓存中存在图片就返回图片并且更新UIImageView.
  • 缓存中不存在图片就异步下载图片,加入缓存,更新UIImageView.

具体流程图:

SDWebImage实现流程图.png

二.架构简介:

A.架构图:

SDWebImageView_relationship.jpeg

UIImageView+WebCaceh和UIButton+WebCache直接为UIkit框架提供接口,而SDWebImageManger负责处理和协调SDWebImageDownloader和SDWebImageCache.并与UIkit层进行交互。

三、具体分析:

1.UIImageView+WebCache

A.框架常用入口

// 所有设置图片最终都会调用这个方法
- (void)sd_setImageWithURL:(NSURL *)url 
      placeholderImage:(UIImage *)placeholder {
  [self sd_setImageWithURL:url 
        placeholderImage:placeholder 
                 options:0 
                progress:nil 
               completed:nil];
  }

该接口调用下面这个方法:

[self   sd_setImageWithURL:placeholderImage:options:progress:completed:]

该方法作为sd_setImageWithURL接口的最终入口,提供了多种参数。

  • url:远程图片的地址
  • placeholder : 预显示图片
  • options :SDWebImageOptions

      typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { 
      //下载失败了会再次尝试下载 
      SDWebImageRetryFailed = 1 << 0,
    
      //当UIScrollView等正在滚动时,延迟下载图片(放置scrollView滚动卡)     
      WebImageLowPriority = 1 << 1,
    
      //只缓存到内存中
      SDWebImageCacheMemoryOnly = 1 << 2, 
    
      // 图片会边下边显示
      SDWebImageProgressiveDownload = 1 << 3, 
    
     // 将硬盘缓存交给系统自带的NSURLCache去处理 
      SDWebImageRefreshCached = 1 << 4,
    
     //后台下载 
      SDWebImageContinueInBackground = 1 << 5,
    
      // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie 
      SDWebImageHandleCookies = 1 << 6,
    
      // 允许不受信任的SSL证书。主要用于测试目的。 
      SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    
      // 默认情况下,image在装载的时候是按照他们在队列中的顺序装载的(就是先进先出).这个flag会把他们移动到队列的前端,并且立刻装载,而不是等到当前队列装载的时候再装载
      SDWebImageHighPriority = 1 << 8,   
    
      // 默认情况下,占位图会在图片下载的时候显示.这个flag开启会延迟占位图显示的时间,等到图片下载完成之后才会显示占位图
      SDWebImageDelayPlaceholder = 1 << 9, 
    
       // 是否transform图片
      SDWebImageTransformAnimatedImage = 1 << 10,
      };
  • progress :下载进度

B.代码分析:

操作的管理:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {

    // 取消当前下载操作
    [self sd_cancelCurrentImageLoad];

    // 动态添加属性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 如果选项非SDWebImageDelayPlaceholder
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            // 设置占位图
            self.image = placeholder;
        });
    }



    if (url.absoluteString.length > 0) {

        // check if activityView is enabled or not
        if ([self showActivityIndicatorView]) {
             // 显示 下载转圈
            [self addActivityIndicator];
       }

        __weak __typeof(self)wself = self;
        id  operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            // 下载完成回调
            // 移除下载进度转圈
            [wself removeActivityIndicator];
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
       });
    }
}

[self sd_cancelCurrentImageLoad];取消当前的下载操作,它表明 SDWebImage 管理操作的方法:
SDWebImage所有的操作实际都是通过一个 operationDictionary 的字典管理,这个字典是动态添加到 UIView 上的一个属性,因为这个 operationDictionary 需要在UIButton 和 UIImageView 上重用,所以需要添加到它们的根类上。

这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:

// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1
[self  sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]

这行代码会取消当前这个UIImageView的所有操作,不会影响之后进行的下载操作。

占位图的实现:

// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #4
if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;
}

当options中没有SDWebImageDelayPlaceholder,UIImageView添加一个占位图image.

获取图片:

 // UIImageView+WebCache
 // sd_setImageWithURL:placeholderImage:options:progress:completed: #8
if (url)

检测传入的URL是否为空,如果非空就调用全局的SDWebImageManager来获取图片:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

下载完成后调用(SDWebImageCompletionWithFinishedBlock)completedBlock 为 UIImageView.image 赋值, 添加上最终所需要的图片.

// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #10

dispatch_main_sync_safe(^{
   if (!wself) return; 
   if (image) { 
      wself.image = image; 
      [wself setNeedsLayout]; 
    } 
else { 
    if ((options & SDWebImageDelayPlaceholder)) {      
         wself.image = placeholder;
          [wself setNeedsLayout]; 
      }
  } 
  if (completedBlock && finished) { 
      completedBlock(image, error, cacheType, url); 
  }
});

最后在返回 operation的同时, 也会向 operationDictionary中添加一个键值对, 来表示操作的正在进行:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

它将operation 存储到operationDictionary 中方便以后的cancel操作。

2. SDWebImageManager

这个类主要用于处理异步下载和图片缓存的类,也可以直接用SDWebImageManager的downloadImageWithURL:options:progress:completed:来直接下载图片。
可以看出这个类主要作用就是为了UIImageView+WebCache 和 SDWebImageDownloader, SDImageCache之间构建一个桥梁,使它们能够更好的协同工作。

A.核心代码分析:

a.SDWebImageManager

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #6
if ([url isKindOfClass:NSString.class]) { 
  url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) { 
  url = nil;
}

这块代码的功能是确定 url是否被正确传入, 如果传入参数的是 NSString类型就会被转换为 NSURL, 如果转换失败, 那么url 会被赋值为空, 这个下载的操作就会出错.

b. SDWebImageCombinedOperation

当 url
被正确传入之后, 会实例一个非常奇怪的 "operation", 它其实是一个遵循 SDWebImageOperation
协议的 NSObject
的子类. 而这个协议也非常的简单:

@protocol SDWebImageOperation 
 - (void)cancel;
@end

SDWebImageOperation只是看着像NSOperation但是它唯一跟NSOperation相同就是都可以响应cancel方法。调用这个类的cancel方法,会使得它持有的两个operation都被cancel。

// SDWebImageCombinedOperation
// cancel #1
- (void)cancel { 
      self.cancelled = YES; 
      if (self.cacheOperation) { 
            [self.cacheOperation cancel]; 
            self.cacheOperation = nil; 
      } 
      if (self.cancelBlock) {
           self.cancelBlock(); 
          _cancelBlock = nil; 
      }
  }

既然获取了url,再通过url获取对应的key

NSString *key = [self cacheKeyForURL:url];

接着通过key在缓存中查找一起是否下载过相同的图片

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { ... }];

这里调用SDImageCache的实例方法 queryDiskCacheForKey:done:来尝试在缓存中获取图片的数据,而这个方法获取的就是货真价实的NSOperation.
如果我们在缓存中查找到对应的图片,那么我们直接调用completedBlock回调块结束这一次图片的下载操作

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47
dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});

如果没有找到就调用SDWebImageDownLoader的实例方法去下载该图片:

id  subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];

如果这个方法返回正确的 downloadedImage ,那么我们就在全局缓存中存储这个图片的数据:

 [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];

并调用completedBlock对UIImageView或者UIButton添加图片。

最后我们将这个subOperation 的 cancel 操作添加到operation.cancelBlock 中,方便操作的取消

operation.cancelBlock = ^{ [subOperation cancel]; }

3. SDWebImageCache

维护了一个内存缓存和一个可选的磁盘缓存,首先看下查询图片缓存的方法:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

该方法主要功能是异步查询图片缓存,先在内存中查找

// SDWebImageCache
// queryDiskCacheForKey:done: #9
UIImage *image = [self imageFromMemoryCacheForKey:key];

// 内存中查找图片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];

}

imageFromMemoryCacheForKey:key 方法会在SDWebImageCache 维护的缓存 memCache 中查找是否有对应的数据,而 memCache 就是一个 NSCache.

如果在内存中没有找到图片的缓存的话,就需要在磁盘中查找。

- (UIImage *)diskImageForKey:(NSString *)key {
   NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; 
   if (data) { 
      UIImage *image = [UIImage sd_imageWithData:data];
       image = [self scaledImageForKey:key image:image];
       if (self.shouldDecompressImages) {
           image = [UIImage decodedImageWithImage:image];
          } 
      return image; 
  }
 else { 
  return nil; 
  }
}

得到图片对应的NSData后还有经过:

  • 根据图片的不同种类,生成对应的UIImage,
  • 根据key值,调整image的Scale值
  • 如果设置图片需要解压缩,需要对图片进行解码

对图片进行存储需要对url进行MD5加密计算生成相应的key值:

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                      r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                      r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

然后用该key作为图片文件名存储在默认路径下:

// 获取缓存路径方法(自己写的)

- (NSString*)getCachePath {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    if (paths.count > 0) {
        NSString *path = [paths[0] stringByAppendingFormat:@"/com.hackemist.SDWebImageCache.default"];
        if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
        }
        return path;
    }else{
        return nil;
    }
}

之前做朋友圈后台发送图片就是先将小图命名,然后根据获取到的七牛的domain和token,拼出url,接着将该url,进行md5加密,加密后存储到SDWebImage 的默认存储路径下,然后在主界面显示存储的小图,后台去进行图片压缩上传任务。

如果在磁盘中找到图片,就将他复制到内存中,以便下次使用:

 UIImage *diskImage = [self diskImageForKey:key];
  if (diskImage && self.shouldCacheImagesInMemory) {
       NSUInteger cost = SDCacheCostForImage(diskImage);
       [self.memCache setObject:diskImage forKey:key cost:cost];
   }

4.SDWebImageDownloader

专用的并且优化的图片异步下载器,主要用来下载图片,下载放在NSOperationQueue中进行,默认maxConcurrentOperationCount为6,timeout时间为15s.

- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock

该方法直接调用了下载进度回调函数:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }

    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });
}

方法会先查看这个 url是否有对应的 callback, 使用的是 downloader,持有的一个字典 URLCallbacks.
如果是第一次添加回调的话, 就会执行 first = YES, 这个赋值非常的关键, 因为 first不为 YES那么 HTTP 请求就不会被初始化, 图片也无法被获取.
然后, 在这个方法中会重新修正在 URLCallbacks中存储的回调块.

通过dispatch_barrier_async函数提交的任务会等它前面的任务执行完才开始,然后它后面的任务必须等它执行完毕才能开始. 必须使用dispatch_queue_create创建的队列才会达到上面的效果.通过该函数来保证每张图片进度顺序。

如果是第一次添加回调块,那么就会直接运行这个createCallBack这个block,而这个block,就是我们在downloadImageWithURL:options:progress:completed: 中传入的回调块.

接着分析下NSMutableURLRequest请求:

 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];

该request 发送了一个http请求,接着又初始化一个SDWebImageDownloaderOperation实例,这个实例用于请求网络资源的操作,是NSOperation的子类:

operation = [[wself.operationClass alloc] initWithRequest:request
                                                      options:options
                                                     progress:^(NSInteger receivedSize, NSInteger expectedSize) {

初始化之后,将该operation添加到NSOperationQueue中。(备注:NSOperation实例只有在调用start方法或者加入NSOperationQueue 才会执行)

[wself.downloadQueue addOperation:operation];

5.SDWebImageDownloaderOperation

这个类主要处理HTTP请求,URL连接的类,当这个类的实例被加入到队列之后,start方法被调用,start方法首先产生一个NSURLConnection,,通过NSURLConnection进行图片的下载,为了确保能够处理下载的数据,需要在后台运行runloop,保证程序不被挂起.

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        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:^{
                __strong __typeof (wself) sself = wself;

            if (sself) {
                    [sself cancel];
                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif

        self.executing = YES;
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        self.thread = [NSThread currentThread];
    }

   [self.connection start];

    if (self.connection) {
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }

        //在主线程发通知,这样也保证在主线程收到通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });

        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
           // Make sure to run the runloop in our background thread so it can process downloaded data
            // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
            //       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }

        if (!self.isFinished) {
            [self.connection cancel];
            [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
        }
    }
    else {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

接下来这个 connection 就会开始运行:

[self.connection start];

它发出一个SDWebImageDownloadStartNotification通知,开启状态栏的请求加载转圈。同时调用NSURLConnectionDataDelegate代理

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

前两个代理会不停的回调 pregressBlock 来提示下载进度。

而最后一个代理方法会在图片下载完成之后调用completionBlock 来完成最后 UIImageView.image的更新,而这里调用的 progressBlock,completionBlock, cancelBlock 都是在之前存储在 URLCallbacks
字典中的.

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
    SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
    @synchronized(self) {
        // 停止 该线程 运行时
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.thread = nil;
        self.connection = nil;
        // 通知停止状态栏转圈请求
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
        });
    }

    if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
        responseFromCached = NO;
    }

    if (completionBlock) {
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
            completionBlock(nil, nil, nil, YES);
        } else if (self.imageData) {
             // 进行缓存
            UIImage *image = [UIImage sd_imageWithData:self.imageData];
            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
            image = [self scaledImageForKey:key image:image];

            // Do not force decoding animated GIFs
            if (!image.images) {
                // 进行解码
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:image];
                }
            }
            if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
            }
            else {
                completionBlock(image, self.imageData, nil, YES);
            }
        } else {
            completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
        }
    }
    self.completionBlock = nil;
    [self done];
}

转换处理图片和进行缓存后,将下载image赋值给控件。

四:最后

送上一张自己喜欢的图片:

路.jpeg

个人小结,烦请大家指正,谢谢。