对SDWebImage的浅分析

来源:互联网 发布:淘宝大学新手开店教程 编辑:程序博客网 时间:2024/06/04 18:18


SDWebImage源代码分析


SDWebImage使用——一个可管理远程图片加载的类库

SDWebImage托管在github上。https://github.com/rs/SDWebImage

这个类库提供一个UIImageView类别以支持加载来自网络的远程图片。

具有缓存管理、异步下载、同一个URL下载次数控制和优化等特征。

SDWebImage类库添加入工程时,一定注意需要添加MapKit.framework,如图所示,因为MKAnnotationView+WebCache.h依赖该framework


SDWebImage库的作用:
通过对UIImageView的类别扩展来实现异步加载替换图片的工作。

主要用到的对象:
1
UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调
2
SDWebImageManager,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache(调用SDImageCache),或者向网络读取对象(调用SDWebImageDownloader
实现SDImageCacheSDWebImageDownloader的回调。
3
SDImageCache,根据URLMD5摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。
4
SDWebImageDownloader,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)

其他类



SDWebImage 支持异步的图片下载+缓存,提供了 UIImageView+WebCacha  category,方便使用。纪录一下 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等前端展示图片。
  5. 如果内存缓存中没有,生成 NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存。
  6. 根据 URLKey在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:
  7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
  8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:
  9. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
  10. 图片下载由 NSURLConnection来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。


  1. connection:didReceiveData: 中利用 ImageIO做了按图片下载进度加载效果。
  2. connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
  3. 图片解码处理在一个 NSOperationQueue完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
  4. 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader
  5. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager告知图片下载完成。
  6. 通知所有的 downloadDelegates下载完成,回调给需要的地方展示图片。
  7. 将图片保存到 SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation完成,避免拖慢主线程。
  8. SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
  9. SDWI 也提供了 UIButton+WebCache  MKAnnotationView+WebCache,方便使用。
  10. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

使用示范的代码:

1.UITableView使用UIImageView+WebCache类(基本应用,UIImageView的一个category

前提#import导入UIImageView+WebCache.h文件,然后在tableviewcellForRowAtIndexPath:方法下:

  1. #import "UIImageView+WebCache.h"  
  2.  
  3. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  
  4. {  
  5.     static NSString *MyIdentifier = @"MyIdentifier";  
  6.   
  7.     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];  
  8.   
  9.     if (cell == nil)  
  10.     {  
  11.         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault  
  12.                                        reuseIdentifier:MyIdentifier] autorelease];  
  13.     }  
  14.   
  15.     // Here we use the new provided setImageWithURL: method to load the web image  
  16.     [cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]  
  17.                    placeholderImage:[UIImage imageNamed:@"placeholder.png"]];  
  18.   
  19.     cell.textLabel.text = @"My Text";  
  20.     return cell;  
  21. }  

基本代码:

  1. [imageView setImageWithURL:[NSURL URLWithString:@<a href="http://www.domain.com/path/image.jpg">http://www.domain.com/path/image.jpg</a>]];  

针对iOS4+目标平台,还可以使用如下块语句:

  1. // Here we use the new provided setImageWithURL: method to load the web image  
  2. [cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]  
  3.                placeholderImage:[UIImage imageNamed:@"placeholder.png"]  
  4.                         success:^(UIImage *image) {... success code here ...}  
  5.                         failure:^(NSError *error) {... failure code here ...}];  

2.     使用SDWebImageManager类:可以进行一些异步加载的工作。

  1. SDWebImageManager *manager = [SDWebImageManager sharedManager];  
  2. UIImage *cachedImage = [manager imageWithURL:url]; // 将需要缓存的图片加载进来  
  3. if (cachedImage) {  
  4.       // 如果Cache命中,则直接利用缓存的图片进行有关操作  
  5.       // Use the cached image immediatly  
  6. else {  
  7.       // 如果Cache没有命中,则去下载指定网络位置的图片,并且给出一个委托方法  
  8.       // Start an async download  
  9.      [manager downloadWithURL:url delegate:self];  
  10. }  

当然你的类要实现SDWebImageManagerDelegate协议,并且要实现协议的webImageManager:didFinishWithImage:方法。

  1. // 当下载完成后,调用回调方法,使下载的图片显示  
  2. - (void)webImageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)image {  
  3.     // Do something with the downloaded image  
  4. }  

3.     独立的异步图像下载
可能会单独用到异步图片下载,则一定要用downloaderWithURL:delegate:来建立一个SDWebImageDownloader实例。

  1. downloader =[SDWebImageDownloader downloaderWithURL:url delegate:self];  

这样SDWebImageDownloaderDelegate协议的方法imageDownloader:didFinishWithImage:被调用时下载会立即开始并完成。


4.    
独立的异步图像缓存

SDImageCache类提供一个创建空缓存的实例,并用方法imageForKey:来寻找当前缓存。

  1. UIImage*myCachedImage = [[SDImageCache sharedImageCache] imageFromKey:myCacheKey];  

存储一个图像到缓存是使用方法storeImage: forKey:

  1. [[SDImageCachesharedImageCache] storeImage:myImage forKey:myCacheKey];  

默认情况下,图像将被存储在内存缓存和磁盘缓存中。如果仅仅是想内存缓存中,要使用storeImage:forKey:toDisk:方法的第三个参数带一负值
来替代。


1.下载opertion

 SDWebImageDownloaderOperation

SDWebImageDownloaderOperation继承自NSOperation

对外只暴漏了一个方法,两个属性

它主要的功能是创建一个下载线程去下载图片,并保持回调的block

待下载完成后进行对应的回调处理


这里思想也比较清晰,下载和网络业务放在这里,

与下载顺序(先进先出,先进后出)缓存逻辑,并发线程数量是独立的。


唯独和外界有交叉的地方是下载开始和下载停止的两个通知,其发出去也是与业务无关的


downloadOperation会创建一个NSUrlConnection,由其负责下载


2.SDWebImageDownloader

这个类负责管理下载operation

它是个单例,负责管理并发下载operationqueue

http的头部信息


该类维护了一个由下载url作为key构成的可变字典

key对应的value是一个数组,此处对应progress blockcompletedBlock也做了保存

下载完整,下载失败等过程结束后,都会根据key在将其在可变字典里的记录移除掉


由于这里的可变字典,可能会比较大,因为并发下载线程同时最多2


        _downloadQueue.maxConcurrentOperationCount = 2;

这里对字典的操作,都是互斥的,并且放在一个gcd线程里,


        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_barrier_sync方式调用


添加的时候,dispatch_barrier_sync(需要关注顺序)

  1. dispatch_barrier_sync(self.barrierQueue, ^  
  2.     {  
  3.         BOOL first = NO;  
  4.         if (!self.URLCallbacks[url])  
  5.         {  
  6.             self.URLCallbacks[url] = NSMutableArray.new;  
  7.             first = YES;  
  8.         }  
  9.   
  10.         // Handle single download of simultaneous download request for the same URL  
  11.         NSMutableArray *callbacksForURL = self.URLCallbacks[url];  
  12.         NSMutableDictionary *callbacks = NSMutableDictionary.new;  
  13.         if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];  
  14.         if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];  
  15.         [callbacksForURL addObject:callbacks];  
  16.         self.URLCallbacks[url] = callbacksForURL;  
  17.   
  18.         if (first)  
  19.         {  
  20.             createCallback();  
  21.         }  
  22.     });  





移除的时候,dispatch_barrier_async(不需要关注顺序)



  1. dispatch_barrier_async(self.barrierQueue, ^  
  2.    {  
  3.        [self.URLCallbacks removeObjectForKey:url];  
  4.    });  




这里用可变字典管理下载任务列表有另外一个好处,就是重复的下载请求可以避免

核心代码如下:



  1.   BOOL first = NO;  
  2.         if (!self.URLCallbacks[url])  
  3.         {  
  4.             self.URLCallbacks[url] = NSMutableArray.new;  
  5.             first = YES;  
  6.         }  
  7.   
  8. ...  
  9.   
  10.         if (first)  
  11.         {  
  12.             createCallback();  
  13.         }  




3.SDImageCache 图片缓存

这个类就管理缓存相关逻辑,本地硬盘缓存,内存缓存,内存告警时候的处理,图片过期处理,后台运行处理

图片内存缓存是用NSCache来管理



  1. _memCache = [[NSCache alloc] init];  


图片硬盘存储是存储在library cache底下的子目录里,注意这个目录apple itunes备份是不保存的,放在这里最合理,图片太多


图片写入到硬盘是放在一个serial queue里,一个线程顺序的写入到本地



  1. if (toDisk)  
  2.  {  
  3.      dispatch_async(self.ioQueue, ^  
  4.      {  
  5.          NSData *data = imageData;  
  6.   
  7.          if (!data)  
  8.          {  
  9.              if (image)  
  10.              {  
  11.  TARGET_OS_IPHONE  
  12.                  data = UIImageJPEGRepresentation(image, (CGFloat)1.0);  
  13. se  
  14.                  data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];  
  15. dif  
  16.              }  
  17.          }  
  18.   
  19.          if (data)  
  20.          {  
  21.              // Can't use defaultManager another thread  
  22.              NSFileManager *fileManager = NSFileManager.new;  
  23.   
  24.              if (![fileManager fileExistsAtPath:_diskCachePath])  
  25.              {  
  26.                  [fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];  
  27.              }  
  28.   
  29.              [fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];  
  30.          }  
  31.      });  
  32.  }  


检查本地硬盘是否有缓存的图片,也是在这个queue里做的




  1. - (BOOL)diskImageExistsWithKey:(NSString *)key  
  2. {  
  3.     __block BOOL exists = NO;  
  4.     dispatch_sync(_ioQueue, ^  
  5.     {  
  6.         exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];  
  7.     });  
  8.       
  9.     return exists;  
  10. }  



删除本地硬盘缓存图片文件:



  1. if (fromDisk)  
  2.  {  
  3.      dispatch_async(self.ioQueue, ^  
  4.      {  
  5.          [[NSFileManager defaultManager] removeItemAtPath:[self defaultCachePathForKey:key] error:nil];  
  6.      });  
  7.  }  



总之,涉及到IO操作的,都是单独的线程来做,这样总体来说用户体验是最佳的。


4. app收到memory warningapp将要退出,以及app到后台运行各种状态下程序处理

  memory warning就是清空本地内存缓存



  1. [[NSNotificationCenter defaultCenter] addObserver:self  
  2.                                           selector:@selector(clearMemory)  
  3.                                               name:UIApplicationDidReceiveMemoryWarningNotification  
  4.                                             object:nil];  


这样可以尽早的释放内存中图片


app将要退出的时候,可以清除本地硬盘上已经过期的图片(缓存时间可以任意指定),超过缓存size的图片(缓存size可以任意指定)

app ios4.0之后可以放在后台运行一段时间

以上的任务也可以放在后台运行一段时间



  1. - (void)backgroundCleanDisk  
  2. {  
  3.     UIApplication *application = [UIApplication sharedApplication];  
  4.     __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^  
  5.     {  
  6.         // Clean up any unfinished task business by marking where you  
  7.         // stopped or ending the task outright.  
  8.         [application endBackgroundTask:bgTask];  
  9.         bgTask = UIBackgroundTaskInvalid;  
  10.     }];  
  11.       
  12.     // Start the long-running task and return immediately.  
  13.     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^  
  14.     {  
  15.         // Do the work associated with the task, preferably in chunks.  
  16.         [self cleanDisk];  
  17.           
  18.         [application endBackgroundTask:bgTask];  
  19.         bgTask = UIBackgroundTaskInvalid;  
  20.     });  
  21. }  




5.SDWebImageManager总体的image 管理类

其对外暴露的接口返回的是id类型,实现了协议(id<SDWebImageOperation>)

返回出去其实是SDWebImageCombinedOperation

它什么都不做,仅仅保存两个东西:(一个block,可以取消下载operation一个operation,去查询本地硬盘缓存的operation

这样就很清晰了,这个对外返回的SDWebImageCombinedOperation,要么查到缓存,要么没查到,可以去下载,也可以取消下载

SDWebImageCombinedOperation封装的很好,给用户看到的是透明的,对外可以找到缓存或者下载到图片,或者由用户决定取消下载图片


6.下载回来的图片解压缩

下载成功的图片会被解压缩,这样在图片渲染到屏幕上,可以比正常的二进制图片数据渲染减少一个步骤,可以提升体验

具体在图片下载完整的时候,就在下载operation线程里,把这件事情做了

NSURLConnection的回调里可以看到



  1. - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection  
  2. {  
  3.     CFRunLoopStop(CFRunLoopGetCurrent());  
  4.     self.connection = nil;  
  5.   
  6.     [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];  
  7.   
  8.     SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;  
  9.   
  10.     if (completionBlock)  
  11.     {  
  12.         if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached)  
  13.         {  
  14.             completionBlock(nil, nil, nilYES);  
  15.             self.completionBlock = nil;  
  16.             [self done];  
  17.         }  
  18.         else  
  19.         {  
  20.               
  21.             UIImage *image = [UIImage sd_imageWithData:self.imageData];  
  22.               
  23.             image = [self scaledImageForKey:self.request.URL.absoluteString image:image];  
  24.               
  25.             if (!image.images) // Do not force decod animated GIFs  
  26.             {  
  27. <span style="color:#ff0000;">                image = [UIImage decodedImageWithImage:image];  
  28. </span>            }  
  29.               
  30.             if (CGSizeEqualToSize(image.size, CGSizeZero))  
  31.             {  
  32.                 completionBlock(nil, nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: @"Downloaded image has 0 pixels"}], YES);  
  33.             }  
  34.             else  
  35.             {  
  36.                 completionBlock(image, self.imageData, nilYES);  
  37.             }  
  38.             self.completionBlock = nil;  
  39.             [self done];  
  40.         }  
  41.     }  
  42.     else  
  43.     {  
  44.         [self done];  
  45.     }  
  46. }  


0 0
原创粉丝点击