gavanwanggw

导航

SDWebImage源代码解析(一)

一、概念

SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类。以支持从远程server下载缓存图片的功能。


二、优势

自从iOS5.0開始。NSURLCache也能够处理磁盘缓存,那么SDWebImage的优势在哪

  1. 首先NSURLCache是缓存原始数据(raw data)到磁盘或内存。因此每次使用的时候须要将原始数据转换成详细的对象。如UIImage等,这会导致额外的数据解析以及内存占用等,而SDWebImage则是缓存UIImage对象在内存。缓存在NSCache中,同一时候直接保存压缩过的图片到磁盘中;
  2. 第一次在UIImageView中使用image对象的时候,图片的解码是在主线程中执行的!

    而SDWebImage会强制将解码操作放到子线程中。


三、功能

  1. 提供UIImageView的一个分类。以支持网络图片的载入与缓存管理
  2. 一个异步的图片载入器
  3. 一个异步的内存+磁盘图片缓存
  4. 支持GIF图片
  5. 支持WebP图片
  6. 后台图片解压缩处理
  7. 确保同一个URL的图片不被下载多次
  8. 确保虚假的URL不会被重复载入
  9. 确保下载及缓存时,主线程不被堵塞
  10. 保证主线程不会死锁
  11. 使用GCD和ARC
本文我们主要从源代码的角度来分析一下SDWebImage的实现机制。

讨论的内容将主要集中在图片的下载及缓存,而不包括对GIF图片及WebP图片的支持操作。


四、源代码解析

1、下载

在SDWebImage中,图片的下载是由SDWebImageDownloader类来完毕的。它是一个异步下载器,并对图像载入做了优化处理。以下我们就来看看它的详细实现。


1.1 下载选项

在下载的过程中。程序会依据设置的不同的下载选项,而运行不同的操作。下载选项由枚举SDWebImageDownloaderOptions定义。详细例如以下

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
  SDWebImageDownloaderLowPriority = 1 << 0,
  SDWebImageDownloaderProgressiveDownload = 1 << 1,
  // 默认情况下请求不使用NSURLCache,假设设置该选项。则以默认的缓存策略来使用NSURLCache
  SDWebImageDownloaderUseNSURLCache = 1 << 2,
  // 假设从NSURLCache缓存中读取图片,则使用nil作为參数来调用完毕block
  SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
  // 在iOS 4+系统上。同意程序进入后台后继续下载图片。

该操作通过向系统申请额外的时间来完毕后台下载。假设后台任务终止,则操作会被取消 SDWebImageDownloaderContinueInBackground = 1 << 4, // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie SDWebImageDownloaderHandleCookies = 1 << 5, // 同意不受信任的SSL证书。主要用于測试目的。

SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, // 将图片下载放到高优先级队列中 SDWebImageDownloaderHighPriority = 1 << 7, };

能够看出,这些选项主要涉及到下载的优先级、缓存、后台任务运行、cookie处理以及认证几个方面。


1.2 下载顺序

SDWebImage的下载操作是按一定顺序来处理的。它定义了两种下载顺序,例如以下所看到的

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
  // 以队列的方式。依照先进先出的顺序下载。这是默认的下载顺序
  SDWebImageDownloaderFIFOExecutionOrder,
  // 以栈的方式,依照后进先出的顺序下载。
  SDWebImageDownloaderLIFOExecutionOrder
};

1.3 下载管理器

SDWebImageDownloader下载管理器是一个单例类。它主要负责图片的下载操作的管理。

图片的下载是放在一个NSOperationQueue操作队列中来完毕的,其声明例如以下:

@property (strong, nonatomic) NSOperationQueue *downloadQueue;

默认情况下,队列最大并发数是6。假设须要的话。我们能够通过SDWebImageDownloader类的 maxConcurrentDownloads 属性来改动。

全部下载操作的网络响应序列化处理是放在一个自己定义的并行调度队列中来处理的,其声明及定义例如以下:

@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
- (id)init {
  if ((self = [super init])) {
    ...
    _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
    ...
  }
  return self;
}
每个图片的下载都会相应一些回调操作,例如以下载进度回调。下载完毕回调等。这些回调操作是以block形式来呈现,为此在SDWebImageDownloader.h中定义了几个block。例如以下所看到的:
// 下载进度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下载完毕
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
// Header过滤
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
图片下载的这些回调信息存储在SDWebImageDownloader类的 URLCallbacks 属性中,该属性是一个字典,key是图片的URL地址。value则是一个数组,包括每一个图片的多组回调信息。因为我们同意多个图片同一时候下载,因此可能会有多个线程同一时候操作URLCallbacks属性。为了保证URLCallbacks操作(加入、删除)的线程安全性,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并设置屏障来确保同一时间仅仅有一个线程操作URLCallbacks属性。我们以加入操作为例,例如以下代码所看到的:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    ...
    // 1. 以dispatch_barrier_sync操作来保证同一时间仅仅有一个线程能对URLCallbacks进行操作
    dispatch_barrier_sync(self.barrierQueue, ^{
  ...
  // 2. 处理同一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;
  ...
    });
}
整个下载管理器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的,该方法调用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入管理器中,同一时候在创建回调的block中创建新的操作。配置之后将其放入downloadQueue操作队列中,最后方法返回新创建的操作。其详细实现例如以下:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
  ...
  [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
    ...
    // 1. 创建请求对象。并依据options參数设置其属性
    // 为了避免潜在的反复缓存(NSURLCache + SDImageCache)。假设没有明白告知须要缓存,则禁用图片请求的缓存操作
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
    ...
    // 2. 创建SDWebImageDownloaderOperation操作对象。并进行配置
    // 配置信息包含是否须要认证、优先级
    operation = [[wself.operationClass alloc] initWithRequest:request
                              options:options
                             progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                               // 3. 从管理器的callbacksForURL中找出该URL全部的进度处理回调并调用
                               ...
                               for (NSDictionary *callbacks in callbacksForURL) {
                                 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                 if (callback) callback(receivedSize, expectedSize);
                               }
                             }
                            completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                               // 4. 从管理器的callbacksForURL中找出该URL全部的完毕处理回调并调用,
                               // 假设finished为YES。则将该url相应的回调信息从URLCallbacks中删除
                              ...
                              if (finished) {
                                [sself removeCallbacksForURL:url];
                              }
                              for (NSDictionary *callbacks in callbacksForURL) {
                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                if (callback) callback(image, data, error, finished);
                              }
                            }
                            cancelled:^{
                              // 5. 取消操作将该url相应的回调信息从URLCallbacks中删除
                              SDWebImageDownloader *sself = wself;
                              if (!sself) return;
                              [sself removeCallbacksForURL:url];
                            }];
    ...
    // 6. 将操作增加到操作队列downloadQueue中
    // 假设是LIFO顺序。则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
    [wself.downloadQueue addOperation:operation];
    if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
      [wself.lastAddedOperation addDependency:operation];
      wself.lastAddedOperation = operation;
    }
  }];
  return operation;
}

另外,每一个下载操作的超时时间能够通过downloadTimeout属性来设置,默认值为15秒。


1.4 下载操作

每一个图片的下载都是一个Operation操作。

我们在上面分析过这个操作的创建及增加操作队列的过程。

如今我们来看看单个操作的详细实现。

SDWebImage定义了一个协议,即 SDWebImageOperation 作为图片下载操作的基础协议。

它仅仅声明了一个cancel方法。用于取消操作。协议的详细声明例如以下:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

SDWebImage自己定义了一个Operation类,即 SDWebImageDownloaderOperation,它继承自NSOperation。并採用了SDWebImageOperation协议。除了继承而来的方法,该类仅仅向外暴露了一个方法。即上面所用到的初始化方法initWithRequest:options:progress:completed:cancelled:。

对于图片的下载,SDWebImageDownloaderOperation全然依赖于URL载入系统中的NSURLConnection类(并未使用7.0以后的NSURLSession类)。我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理。即NSURLConnection各代理方法的实现。

首先,SDWebImageDownloaderOperation在分类中採用了NSURLConnectionDataDelegate协议,并实现了该协议的下面几个方法:

- connection:didReceiveResponse:
- connection:didReceiveData:
- connectionDidFinishLoading:
- connection:didFailWithError:
- connection:willCacheResponse:
- connectionShouldUseCredentialStorage:
- connection:willSendRequestForAuthenticationChallenge:

我们在此不逐一分析每一个方法的实现。就重点分析一下-connection:didReceiveData:方法。该方法的主要任务是接收数据。

每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理。在首次获取到数据时(width+height==0)会从这些包括图像信息的数据中取出图像的长、宽、方向等信息以备使用。

而后在图片下载完毕之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完毕回调使用。当然。在这种方法中还须要处理的就是进度信息。

假设我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
  // 1. 附加数据
  [self.imageData appendData:data];
  if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
    // 2. 获取已下载数据总大小
    const NSInteger totalSize = self.imageData.length;
    // 3. 更新数据源,我们须要传入全部数据。而不不过新数据
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
    // 4. 首次获取到数据时,从这些数据中获取图片的长、宽、方向属性值
    if (width + height == 0) {
      CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
      if (properties) {
        NSInteger orientationValue = -1;
        CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
        if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
        ...
        CFRelease(properties);
        // 5. 当绘制到Core Graphics时。我们会丢失方向信息,这意味着有时候由initWithCGIImage创建的图片
        //	的方向会不正确。所以在这边我们先保存这个信息并在后面使用。
        orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
      }
    }
    // 6. 图片还未下载完毕
    if (width + height > 0 && totalSize < self.expectedSize) {
      // 7. 使用现有的数据创建图片对象。假设数据中存有多张图片。则取第一张
      CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
      // 8. 适用于iOS变形图像的解决方式。

我的理解是因为iOS只支持RGB颜色空间,所以在此对下载下来的图片做个颜色空间转换处理。 if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif // 9. 对图片进行缩放、解码操作 if (partialImageRef) { UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; UIImage *scaledImage = [self scaledImageForKey:key image:image]; image = [UIImage decodedImageWithImage:scaledImage]; CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } CFRelease(imageSource); } if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); } }

注:缩放操作能够查看SDWebImageCompat文件里的SDScaledImageForKey函数;

解压缩操作能够查看SDWebImageDecoder文件+decodedImageWithImage方法

//SDWebImageCompat
//兼容类,这个类定义了非常多宏另一个伸缩图片的方法。宏就不说了
//这种方法定义成C语言式的内联方法核心代码例如以下。传入key和图片,假设key中出现@2x就设定scale为2.0,出现@3x就设定scale为3.0,然后伸缩图片
CGFloat scale = [UIScreen mainScreen].scale;
if (key.length >= 8) {
    NSRange range = [key rangeOfString:@"@2x."];
    if (range.location != NSNotFound) {
        scale = 2.0;
    }

    range = [key rangeOfString:@"@3x."];
    if (range.location != NSNotFound) {
        scale = 3.0;
    }
}

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

//SDWebImageDecoder
//这个是解码器类,仅仅定义了一个解码方法,传入图片,返回的也是图片
//CGImageRef是一个指针类型。

//typedef struct CGImage *CGImageRef;获取传入图片的alpha信息,然后推断是否符合苹果定义的CGImageAlphaInfo,假设是就返回原图片 CGImageRef imageRef = image.CGImage; CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); if (anyAlpha) { return image; } //然后获取图片的宽高和color space(指定颜色值怎样解释),推断color space是否支持,不支持就转换为支持的模式(RGB),再用图形上下文依据获得的信息画出来。释放掉创建的CG指针再返回图片 size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); // current CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed); if (unsupportedColorSpace) colorspaceRef = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, width, height, CGImageGetBitsPerComponent(imageRef), 0, colorspaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(context); UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation]; if (unsupportedColorSpace) CGColorSpaceRelease(colorspaceRef); CGContextRelease(context); CGImageRelease(imageRefWithAlpha); return imageWithAlpha;


我们前面说过SDWebImageDownloaderOperation类是继承自NSOperation类。它没有简单的实现main方法,而是採用更加灵活的start方法。以便自己管理下载的状态。

在start方法中,创建了我们下载所使用的NSURLConnection对象。开启了图片的下载,同一时候抛出一个下载開始的通知。当然。假设我们期望下载在后台处理,则仅仅须要配置我们的下载选项,使其包括SDWebImageDownloaderContinueInBackground选项。start方法的详细实现例如以下:

- (void)start {
  @synchronized (self) {
    // 管理下载状态,假设已取消,则重置当前下载并设置完毕状态为YES
    if (self.isCancelled) {
      self.finished = YES;
      [self reset];
      return;
    }
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    // 1. 假设设置了在后台运行,则进行后台运行
    if ([self shouldContinueWhenAppEntersBackground]) {
      __weak __typeof__ (self) wself = self;
      self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        ...
        }
      }];
    }
#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);
    }
    // 2. 在主线程抛出下载開始通知
    dispatch_async(dispatch_get_main_queue(), ^{
      [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
    });
    // 3. 启动run loop
    if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
      CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
    }
    else {
      CFRunLoopRun();
    }
    // 4. 假设未完毕。则取消连接
    if (!self.isFinished) {
      [self.connection cancel];
      [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
    }
  }
  else {
    ... 
  }
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
  if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
    [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
    self.backgroundTaskId = UIBackgroundTaskInvalid;
  }
#endif
}

当然,在下载完毕或下载失败后。须要停止当前线程的run loop。清除连接,并抛出下载停止的通知。

假设下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完毕回调使用。

详细可參考-connectionDidFinishLoading:与-connection:didFailWithError:的实现。


1.5 小结

下载的核心事实上就是利用NSURLConnection对象来载入数据。每一个图片的下载都由一个Operation操作来完毕,并将这些操作放到一个操作队列中。

这样能够实现图片的并发下载。





參考:
  1. http://southpeak.github.io/blog/2015/02/07/yuan-ma-pian-:sdwebimage/?utm_source=tuicool&utm_medium=referral
  2. http://blog.sina.com.cn/s/blog_8988732e0101af25.html
  3. http://www.jianshu.com/p/c07df06c60be
  4. http://www.zuimoban.com/jiaocheng/ios/2016/0310/6794.html

posted on 2017-08-01 13:57  gavanwanggw  阅读(153)  评论(0编辑  收藏  举报