iOS SDWebImage 学习

6,449 阅读8分钟

官方SDWebImage的架构图

官方SDWebImage的架构图

SDWebImage库的作用:

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

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

SDWebImage 缓存流程

官方SDWebImage的流程图

以最为常用的UIImageView为例:

  1. UIImageView+WebCache:  setImageWithURL:placeholderImage:options: 先显示 placeholderImage ,同时由SDWebImageManager 根据 URL 来在本地查找图片。
  2. SDWebImageManager: downloadWithURL:delegate:options:userInfo: SDWebImageManager是将UIImageView+WebCache同SDImageCache链接起来的类, SDImageCache: queryDiskCacheForKey:delegate:userInfo:用来根据CacheKey查找图片是否已经在缓存中
  3. 如果内存中已经有图片缓存, SDWebImageManager会回调SDImageCacheDelegate : imageCache:didFindImage:forKey:userInfo:
  4. 而 UIImageView+WebCache 则回调SDWebImageManagerDelegate:  webImageManager:didFinishWithImage:来显示图片。
  5. 如果内存中没有图片缓存,那么生成 NSInvocationOperation 添加到队列,从硬盘查找图片是否已被下载缓存。
  6. 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
  7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
  8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
  9. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
  10. 图片下载由 NSURLSession 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
  11. connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
  12. connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
  13. 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
  14. 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
  15. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
  16. 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
  17. 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。
  18. 写文件到硬盘在单独 NSInvocationOperation 中完成,避免拖慢主线程。
  19. 如果是在iOS上运行,SDImageCache 在初始化的时候会注册notification 到UIApplicationDidReceiveMemoryWarningNotification 以及 UIApplicationWillTerminateNotification,在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片。
  20. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

 

SDWebImage 使用

  • 查看缓存大小

    - (NSString *)readSDWebImageCache {
        NSUInteger size = [SDImageCache sharedImageCache].getSize;
        // 1k = 1024, 1m = 1024k
        if (size < 1024) { // 小于1k
            return [NSString stringWithFormat:@"%ldB",(long)size];
        }else if (size < 1024 * 1024) { // 小于1m
            CGFloat aFloat = size/1024;
            return [NSString stringWithFormat:@"%.0fK",aFloat];
        }else if (size < 1024 * 1024 * 1024) { // 小于1G
            CGFloat aFloat = size/(1024 * 1024);
            return [NSString stringWithFormat:@"%.1fM",aFloat];
        }else {
            CGFloat aFloat = size/(1024*1024*1024);
            return [NSString stringWithFormat:@"%.1fG",aFloat];
        }
    }
    
  • 清除缓存

    - (void)clearDisk {
        NSLog(@"SDWebImageCache---%@", [self readSDWebImageCache]);
        [[SDImageCache sharedImageCache] clearDiskOnCompletion:nil];
        [[SDImageCache sharedImageCache] clearMemory]; //可不写
        NSLog(@"SDWebImageCache2---%@", [self readSDWebImageCache]);
    }
    

 

SDWebImage 缓存

  • 清理缓存图片的策略:

    特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
    注意:它默认只支持超过7天的图片清除。不对图片缓存大小进行控制。当然它已经做了这种机制,只是maxCacheSize为默认值0,所以不生效。
    1. 遍历缓存目录使用下面函数
    NSArray *resourceKeys = @[NSURLIsDirectoryKey,   NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    
    NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                includingPropertiesForKeys:resourceKeys                                                              options:NSDirectoryEnumerationSkipsHiddenFiles
                                                              errorHandler:NULL];
    
    1. 归档过期缓存
     for (NSURL *fileURL in fileEnumerator) {
         ......
         // 根据文件路径最后修改时间来获取内容
         NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
     // 判断是否过缓存期
         if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate])         {
             [urlsToDelete addObject:fileURL];
             continue;
         }
    
         // 这里同时对未过期的文件根据文件大小进行归档,便以后续重置缓存.
         NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
         currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
         [cacheFiles setObject:resourceValues forKey:fileURL];
     }
    
    1. 删除过期缓存
     for (NSURL *fileURL in urlsToDelete) {
         [_fileManager removeItemAtURL:fileURL error:nil];
     } 
    
    1. 重置缓存大小
    // 依据文件修改时间,对未过期的文件进行升序排序.
     NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                     }];
     
     // 根据设定的缓存大小,对当前缓存进行调整,删除那些快过期的文件,使当前总的文件大小小与设定的缓存大小。
     for (NSURL *fileURL in sortedFiles) {
         if ([_fileManager removeItemAtURL:fileURL error:nil]) {
             NSDictionary *resourceValues = cacheFiles[fileURL];
             NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
             currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
             
             if (currentCacheSize < desiredCacheSize) {
                 break;
             }
         }
     }
    
  • app事件注册使用经典的观察者模式,当观察到内存警告、程序被终止、程序进入后台这些事件时,程序将自动调用相应的方法处理

    当收到系统内存告警通知时,对内存缓存进行处理
    [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(clearMemory)
                                                   name:UIApplicationDidReceiveMemoryWarningNotification
                                                 object:nil];
                                                 
                                                 
    
    当进程终止时,对缓存文件进行处理
    [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(cleanDisk)
                                                   name:UIApplicationWillTerminateNotification
                                                 object:nil];
    
    当进入后台运行时,对缓存文件进行处理
    [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(backgroundCleanDisk)
                                                   name:UIApplicationDidEnterBackgroundNotification
                                                 object:nil];
    

 

SDWebImage 源码解析

SDImageDownloader负责管理所有的下载任务,具体的下载任务由SDImageDownloaderOperation类负责。

  • SDWebImageDownloaderOperation
    SDWebImageDownloaderOperation源码解析
    开发者就可以不使用SDWebImage提供的下载任务类,而可以自定义相关类,只需要遵守协议即可,SDWebImageDownloaderOperation类也遵守了该协议,该类继承自 NSOperation 主要是为了将任务加进并发队列里实现多线程下载多张图片,真正实现下载操作的是 NSURLSessionTask 类的子类,这里就可以看出 SDWebImage 使用 NSURLSession 实现下载图片的功能
  • SDImageDownloader
    SDImageDownloader源码解析
    SDWebImage主要使用了自定义NSOperation子类,并在这个自定义NSOperation子类中通过一个可用的NSURLSession来创建一个执行服务器交互数据的NSURLSessionDataTask的下载任务,并由其全权负责下载工作,接着使用NSOperationQueue实现多线程的多图片下载。

 

其他

  • 常见SDWebImageOptions
    SDWebImageRetryFailed, 下载失败后会自动重新下载
    SDWebImageLowPriority, 当正在与UI进行交互时,自动暂停内部的一些下载功能
    SDWebImageRetryFailed | SDWebImageLowPriority,同时存在上边两种
    SDWebImageCacheMemoryOnly, 取消磁盘缓存只有内存缓存
    SDWebImageProgressiveDownload,默认情况,图像会在下载完成后一次性显示
  • 默认存储法都是是内存缓存和磁盘缓存结合的方式。如果你只需要内存缓存,那么在带options选项的方法options这里选择SDWebImageCacheMemoryOnly就可以了
  • 对于图片的缓存实际应用的是NSURLCache自带的cache机制。NSURLCache每次都要把缓存的raw data 再转化为UIImage
  • SDWebImage提供了如下三个category来进行缓存
    MKAnnotationView(WebCache)
    UIButton(WebCache)
    UIImageView(WebCache)
  • 比如在下载某个图片的过程中要响应一个事件,就覆盖这个方法:
    [[SDWebImageManager sharedManager].imageDownloader downloadImageWithURL:urlPath options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
       NSLog(@"下载进度---%f", (float)receivedSize/expectedSize);
    } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
       NSLog(@"下载完成---%@", [NSThread currentThread]);
    }];
    
  • 图片下载速度不一致,用户快速滚动的时候,会因为cell重用导致图片混乱
    解决办法:MVC,使用模型保持下载的图像,再次刷新表格。
  • 将图像保存到模型里的优缺点
    优点:不用重复下载,利用MVC刷新表格,不会造成数据混乱,加载速度比较快
    缺点:内存。所有下载好的图像,都会记录在模型里。如果数据比较多(2000)造成内存警告
  • 图片格式简介
    PNG:无损压缩,压缩比较低,PNG图片一般会比JPG大。(GPU解压缩的消耗非常小,解压缩的速度比较快,比较清晰,苹果推荐使用)
    JPG:有损压缩!压缩比非常高!照相机使用(GPU解压缩的消耗非常大)
    GIF:动图
    BMP:位图,没有任何压缩,几乎不用
  • SDWebImage自己的编解码技术
    在展示一张图片的时候常使用imageNamed:这样的类方法去获取并展示这张图片,但是图片是以二进制的格式保存在磁盘或内存中的,如果要展示一张图片需要根据图片的不同格式去解码为正确的位图交由系统控件来展示,而解码的操作默认是放在主线程执行,凡是放在主线程执行的任务都务必需要考虑清楚,如果有大量图片要展示,就会在主线程中执行大量的解码任务,势必会阻塞主线程造成卡顿,所以SDWebImage自己实现相关的编解码操作,并在子线程中处理,就不会影响主线程的相关操作

更多实用详见 Demo VenderExplore文件夹下