SDWebImage 解析笔记

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

iOS 源代码分析 --- SDWebImage

SDWebImage源码剖析(-)

SDWebImage源码剖析(二)

蝶.jpg

一. 简介

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

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

主要用到的对象:

1、UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调

2、SDWebImageManager,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache(调用SDImageCache),或者向网络请求下载对象(调用SDWebImageDownloader) 。
实现SDImageCacheSDWebImageDownloader的回调。

3、SDImageCache,根据URL的MD5生成key对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。

4、SDWebImageDownloader,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)

其他类:
SDWebImageDecoder,异步对图像进行了一次解压。

具体流程图:


SDWebImage实现流程图.png

SDWebImage 加载图片的流程 :

  1. 入口 setImageWithURL:placeholderImage:options:会先把placeholderImage显示,然后 SDWebImageManager 根据 URL 开始处理图片。

  2. 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

  3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo:SDWebImageManager

4.SDWebImageManagerDelegate回调 webImageManager:didFinishWithImage:UIImageView+WebCache 等前端展示图片。

  1. 如果内存缓存中没有,生成NSInvocationOperation 添加到队列开始从硬盘异步查找图片是否已经缓存。

  2. 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:

  3. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

  4. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:

  5. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

  6. 图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

  7. connection:didReceiveData: 中利用ImageIO 做了按图片下载进度加载效果。

  8. connectionDidFinishLoading:数据下载完成后交给 SDWebImageDecoder做图片解码处理。

  9. 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

  10. 在主线程 notifyDelegateOnMainThreadWithInfo:宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:回调给 SDWebImageDownloader

  11. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

  12. 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

  13. 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

  14. SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

  15. SDWebImage 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

  16. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

二. 架构简介

A.架构图:

SDWebImageView_relationship.jpeg

UIImageView+WebCacehUIButton+WebCache直接为UIkit框架提供接口,而SDWebImageManger负责处理和协调SDWebImageDownloaderSDWebImageCache并与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 : 预显示图片

  • optionsSDWebImageOptions

      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 <SDWebImageOperation> 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 需要在UIButtonUIImageView 上重用,所以需要添加到它们的根类上。

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

// 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操作。

清晨.jpg

2. SDWebImageManager

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

A.核心代码分析:

a.SDWebImageManager

// SDWebImageManager
//- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:

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 <NSObject>
 - (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 <SDWebImageOperation> 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];

并调用completedBlockUIImageView或者UIButton添加图片。

最后我们将这个subOperationcancel 操作添加到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.

NSCache 是一个类似于 NSMutableDictionary 存储 key-value 的容器,主要有以下几个特点:

自动删除机制:当系统内存紧张时,NSCache会自动删除一些缓存对象
线程安全:从不同线程中对同一个 NSCache 对象进行增删改查时,不需要加锁
不同于 NSMutableDictionaryNSCache存储对象时不会对key进行 copy 操作

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

- (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值,调整imageScale
  • 如果设置图片需要解压缩,需要对图片进行解码

对图片进行存储需要对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];
   }



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

树.jpg

4.SDWebImageDownloader

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

- (id <SDWebImageOperation>)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的更新,而这里调用的 progressBlockcompletionBlockcancelBlock都是在之前存储在 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赋值给控件。

四. 面试点

1、SDImageCache是怎么做数据管理的?

  • SDImageCache分成两部分,一个是内存层面的,一个是磁盘层面的。

  • 内存缓存的处理是使用NSCache对象来实现的。NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类,用搜索文件系统的方式做管理,文件替换方式是以时间为单位。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。

  • 磁盘缓存的处理则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹,文件替换方式是以时间为单位,剔除时间大一一周的图片文件。

  • SDWebImageManagerSDImageCache 要资源时, 先搜索内存层面的数据,如果有直接返回,没有再访问磁盘,如果有将图片从磁盘读取出来,然后做解压,将图片对象放到内存层面做备份,再返回调用层。

  1. 为什么图片要进行解压?
  • 因为UIImageimageWithData函数是每次画图的时候才将Data解压成ARGB图像,所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求,为了提高效率,通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上面,这样这张图片就不需要重复解压了,这种做法就是典型的空间换取时间的做法。

3.SDWebImage 在多线程下载图片时防止错乱的策略

  • SDWebImage 会将ImageView 对象关联一个下载列表(列表是给AnimationImages用的,这个时候会下载多张图片),当tableView滚动时,imageView会重设数据源url,这时会cancel掉下载列表中当前对应的下载任务,然后开启一个新的下载任务,这样就保证只有当前可见的cell对象的ImageView对象关联的下载任务能够回调,不会发生Image错乱。

  • 同时,SDWebImage 管理了一个全局下载队列SDWebDownloadManager,并发量设置为6,也就表示如果cell的数目大于6,就会有部分下载队列处于等待状态,而且,在添加下载任务到全局的下载队列中去的时候,SDWebImage 默认采取的是LIFO(后进先出)策略,具体是添加新的下载任务的时候,将之前的下载任务添加依赖为新的下载任务。

另外解决方案:

  • imageView对象和图片的url相关联,在滑动时,不取消旧的下载任务,而是在下载任务完成回调时,进行url匹配,只有匹配成功的image会刷新imageView对象,而其他的image则只做缓存操作,而不刷新UI

  • 同时,仍然管理一个执行队列,为了避免占用太多的资源,通常会对执行队列设置一个最大的并发量。此外,为了保证LIFO的下载策略,可以自己维持一个等待队列,每次下载任务开始的时候,将后进入的下载任务插入到等待队列的前面。

  1. SDWebImage的主要任务就是图片的下载和缓存。为了支持这些操作,它主要使用了以下知识点:
  • dispatch_barrier_sync函数:该方法用于对操作设置屏障,确保在执行完任务后才会执行后续操作。该方法常用于确保类的线程安全性操作。

  • NSMutableURLRequest:用于创建一个网络请求对象,我们可以根据需要来配置请求报头等信息。

NSOperationNSOperationQueue:操作队列是Objective-C中一种高级的并发处理方法,现在它是基于GCD来实现的。相对于GCD来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面也容易一些。对SDWebImage中我们就看到了如何使用依赖将下载顺序设置成后进先出的顺序。

  • NSURLConnection:用于网络请求及响应处理。在iOS7.0后,苹果推出了一套新的网络请求接口,即NSURLSession类。

开启一个后台任务。

  • NSCache类:一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。

  • 清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。

  • 对图片的解压缩操作:这一操作可以查看SDWebImageDecoder.m+decodedImageWithImage方法的实现。

  • GIF图片的处理

  • WebP图片的处理

  1. 系统级内存警告如何处理
  • 取消当前正在进行的所有下载操作[[SDWebImageManager sharedManager] cancelAll];
  • 清除缓存数据:
    内存缓存:直接删除文件,重新创建新的文件
    磁盘缓存:删除过期的文件数据,计算当前未过期的已经下载的文件数据的大小,如果发现该数据大小大于我们设置的最大缓存数据大小,那么程序内部会按照按文件数据缓存的时间从远到近删除,知道小于最大缓存数据为止。
  1. 如何播放gif图片
  • 把用户传入的gif图片->NSData
  • 根据该Data创建一个图片数据源(NSData->CFImageSourceRef
  • 计算该数据源中一共有多少帧,把每一帧数据取出来放到图片数组中
  • 根据得到的数组+计算的动画时间-》可动画的image
    • [UIImage animatedImageWithImages:images duration:duration];
  1. 如何判断当前图片类型
    + (NSString *)sd_contentTypeForImageData:(NSData *)data;
    图片的十六进制数据, 的前8个字节都是一样的, 所以可以通过判断十六进制来判断图片的类型

五. 最后

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

风景.jpeg

个人小结,有兴趣的朋友可以看一下,如果觉得不错,麻烦给个喜欢或star,若发现问题请及时反馈,谢谢!

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

推荐阅读更多精彩内容