美文网首页面试题
SDWebImage原理

SDWebImage原理

作者: 凡几多 | 来源:发表于2018-09-28 15:56 被阅读17次

    一、简介

    1. 设计目的

    SDWebImage 提供了 UIImageView、UIButton 、MKAnnotationView 的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。

    2. 特性

    • 提供 UIImageView, UIButton, MKAnnotationView 的分类,用来显示网络图片,以及缓存管理
    • 异步下载图片
    • 异步缓存(内存+磁盘),并且自动管理缓存有效性
    • 后台图片解压缩
    • 同一个 URL 不会重复下载
    • 自动识别无效 URL,不会反复重试
    • 不阻塞主线程
    • 高性能
    • 使用 GCD 和 ARC
    • 支持多种图片格式(包括 WebP 格式)
    • 支持动图(GIF)(4.0 之前的动图效果并不是太好,4.0 以后基于 FLAnimatedImage加载动图)

    3. 用法

    3.1 UITableView 中使用 UIImageView+WebCache

    UITabelViewCell 中的 UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可

    3.2 使用回调 blocks

    在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
    placeholderImage:[UIImage imageNamed:@"placeholder.png"]
    completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
    //... completion code here ...
    }];
    

    5.3 SDWebImageManager 的使用

    UIImageView(WebCache) 分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。SDWebImageManager也可以单独使用。

        SDWebImageManager *manager = [SDWebImageManager sharedManager];
        [manager loadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            
        } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
            if (image) {
                // do something with image
            }
        }];
    

    5.4 单独使用 SDWebImageDownloader 异步下载图片

    我们还可以单独使用 SDWebImageDownloader 来下载图片,但是图片内容不会缓存。

        SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
        [downloader downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            // progression tracking code
            
        } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
            if (image && finished) {
                // do something with image
            }
        }];
    

    5.5 单独使用 SDImageCache 异步缓存图片

    SDImageCache 支持内存缓存和异步的磁盘缓存(可选),如果你想单独使用 SDImageCache 来缓存数据的话,可以使用单例,也可以创建一个有独立命名空间的 SDImageCache 实例。

    添加缓存的方法:
    [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

    默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:
    [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];
    读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。

    SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
    [imageCache queryCacheOperationForKey:myCacheKey done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
            // image is not nil if image was found
    }];
    

    5.6 自定义缓存 key
    有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的 key。

    SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
     url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
     return [url absoluteString];
    };
    

    二、原理

    SDWebImage.png SDWebImage整体组件结构.png SDWebImage全图.jpeg 官方给的设置图片后执行时序图: 执行时序图.png

    在了解细节之前我们先大概浏览一遍主流程,也就是最核心的逻辑。

    我们从 MasterViewController 中的[cell.imageView sd_setImageWithURL:url placeholderImage:placeholderImage];开始看起。
    经过层层调用,直到 UIImageView+WebCache 中最核心的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:。该方法中,主要做了以下几件事:

    • 取消当前正在进行的加载任务 operation
    • 设置 placeholder
    • 如果 URL 不为 nil,就通过 SDWebImageManager 单例开启图片加载任务 operation,SDWebImageManager 的图片加载方法中会返回一个 SDWebImageCombinedOperation 对象,这个对象包含一个 cacheOperation 和一个 cancelBlock。
    流程简述
    • 1、当我门需要获取网络图片的时候,我们首先需要的便是URL,没有URL什么都没有,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URL相关的图片,如果有则直接返回image,如果没有则进行下一步。
    • 2、当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。
    • 3、如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果正在下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。
    • 4、刷新UI(当然根据实际情况操作)将image存入沙盒缓存。 SD流程.png
    SDWebImageManager 的图片加载方法

    downloadImageWithURL:options:progress:completed: 中会先拿图片缓存的 key (这个 key 默认是图片 URL)去 SDImageCache 单例中读取内存缓存,如果有,就返回给 SDWebImageManager;如果内存缓存没有,就开启异步线程,拿经过 MD5 处理的key 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存中去,然后再返回给 SDWebImageManager

    如果内存缓存和磁盘缓存中都没有,SDWebImageManager 就会调用 SDWebImageDownloader 单例的 -downloadImageWithURL: options: progress: completed:方法去下载,该会先将传入的 progressBlockcompletedBlock 保存起来,并在第一次下载该 URL 的图片时,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloader 的downloadQueue 来启动异步下载任务。

    SDWebImageDownloaderOperation 中包装了一个 NSURLConnection 的网络请求,并通过 runloop 来保持 NSURLConnection 在 start 后、收到响应前不被干掉,下载图片时,监听 NSURLConnection 回调的-connection:didReceiveData:方法中会负责 progress 相关的处理和回调,- connectionDidFinishLoading:方法中会负责将 data 转为 image,以及图片解码操作,并最终回调 completedBlock

    SDWebImageDownloaderOperation 中的图片下载请求完成后,会回调给 SDWebImageDownloader,然后 SDWebImageDownloader 再回调给 SDWebImageManagerSDWebImageManager 中再将图片分别缓存到内存和磁盘上(可选),并回调给 UIImageViewUIImageView 中再回到主线程设置 image 属性。至此,图片的下载和缓存操作就圆满结束了。

    当然,SDWebImage 中还有很多细节可以深挖,包括一些巧妙设计和知识点,接下来再看看SDWebImage 中的实现细节。

    查看SDWebImage的源码,与缓存有关的一共有四个文件SDImageCacheConfig和SDImageCache,首先看一下SDImageCacheConfig的头文件:

    //是否压缩图片,默认为YES,压缩图片可以提高性能,但是会消耗内存
    @property (assign, nonatomic) BOOL shouldDecompressImages;
     
    //是否关闭iCloud备份,默认为YES
    @property (assign, nonatomic) BOOL shouldDisableiCloud;
     
    //是否使用内存做缓存,默认为YES
    @property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
     
    /** 缓存图片的最长时间,单位是秒,默认是缓存一周
     * 这个缓存图片最长时间是使用磁盘缓存才有意义
     * 使用内存缓存在前文中讲解的几种情况下会自动删除缓存对象
     * 超过最长时间后,会将磁盘中存储的图片自动删除
     */
    @property (assign, nonatomic) NSInteger maxCacheAge;
     
    //缓存占用最大的空间,单位是字节
    @property (assign, nonatomic) NSUInteger maxCacheSize;
    

    NSCacheConfig类可以看得出来就是一个配置类,保存一些缓存策略的信息,没有太多可以讲解的地方,看懂就好,看一下NSCacheConfig.m文件的源码:

    static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
    
    @implementation SDImageCacheConfig
    - (instancetype)init {
        if (self = [super init]) {
            _shouldDecompressImages = YES;
            _shouldDisableiCloud = YES;
            _shouldCacheImagesInMemory = YES;
            _maxCacheAge = kDefaultCacheMaxCacheAge;
            _maxCacheSize = 0;
        }
        return self;
    }
    @end
    

    从上面源码可以看出相关属性的默认值,以及maxCacheAge的默认值为一周时间。

    /*
    SDWebImage真正执行缓存的类
    SDImageCache支持内存缓存,默认也可以进行磁盘存储,也可以选择不进行磁盘存储
    */
    @interface SDImageCache : NSObject
    
    #pragma mark - Properties
    
    //SDImageCacheConfig对象,缓存策略的配置
    @property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
    
    //内存缓存的最大cost,以像素为单位,后面有具体计算方法
    @property (assign, nonatomic) NSUInteger maxMemoryCost;
    
    //内存缓存,缓存对象的最大个数
    @property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
    

    上面这一部分是属性的声明,属性很少,但我们在NSCache中都见过了,首先是SDImageCacheConfig,即前面讲解的缓存策略配置,maxMemoryCost其实就是NSCache的totalCostLimit,这里它使用像素为单位进行计算,maxMemoryCountLimit其实就是NSCache的countLimit,需要注意的是SDImageCache继承自NSObject没有继承NSCache,所以它需要保存这些属性。

    /*
    真正执行存储操作的方法
    */
    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        //如果image为nil或image的URL为空直接返回即不执行保存操作
        if (!image || !key) {
            //如果回调块存在就执行完成回调块
            if (completionBlock) {
                completionBlock();
            }
            return;
        }
        // if memory cache is enabled
        //如果缓存策略指明要进行内存缓存
        if (self.config.shouldCacheImagesInMemory) {
            //根据前面的内联函数计算图片的大小作为cost
            NSUInteger cost = SDCacheCostForImage(image);
            //向memCache中添加图片对象,key即图片的URL,cost为上面计算的
            [self.memCache setObject:image forKey:key cost:cost];
        }
    
        //如果要保存到磁盘中
        if (toDisk) {
            //异步提交任务到串行的ioQueue中执行
            dispatch_async(self.ioQueue, ^{
                //进行磁盘存储的具体的操作,使用@autoreleasepool包围,执行完成后自动释放相关对象
                //我猜测这么做是为了尽快释放产生的局部变量,释放内存
                @autoreleasepool {
                    NSData *data = imageData;
                    //如果传入的imageData为空,图片不为空
                    if (!data && image) {
                        // If we do not have any data to detect image format, use PNG format
                        //调用编码方法,获取NSData对象
                        //图片编码为NSData不在本文的讲述范围,可自行查阅
                        data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
                    }
                    //调用下面的方法用于磁盘存储操作
                    [self storeImageDataToDisk:data forKey:key];
                }
                //存储完成后检查是否存在回调块
                if (completionBlock) {
                    //异步提交在主线程中执行回调块
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                }
            });
        //如果不需要保存到磁盘中判断后执行回调块
        } else {
            if (completionBlock) {
                completionBlock();
            }
        }
    }
    
    //具体执行磁盘存储的方法
    - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
        //判断图片NSData数据以及图片key是否为空,如果为空直接返回
        if (!imageData || !key) {
            return;
        }
        //检查当前执行队列是否为ioQueue,如果不是会提示开发者
        [self checkIfQueueIsIOQueue];
    
        //如果构造函数中构造的磁盘缓存存储图片路径的文件夹不存在
        if (![_fileManager fileExistsAtPath:_diskCachePath]) {
            //那就根据这个路径创建需要的文件夹
            [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
    
        // get cache Path for image key
        // 根据key获取默认磁盘缓存存储路径下的MD5文件名的文件的绝对路径
        // 感觉有点绕口。。就是获取图片二进制文件在磁盘中的绝对路径,名称就是前面使用MD5散列的,路径就是构造函数默认构造的那个路径
        NSString *cachePathForKey = [self defaultCachePathForKey:key];
        // transform to NSUrl
        // 根据这个绝对路径创建一个NSURL对象
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        //使用NSFileManager创建一个文件,文件存储的数据就是imageData
        //到此,图片二进制数据就存储在了磁盘中了
        [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
        // disable iCloud backup
        if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    

    上面就是图片缓存存储的核心方法了,如果要进行内存缓存就直接添加到memCache对象中,如果要进行磁盘缓存,就构造一个路径,构造一个文件名,然后存储起来就好了。这里面有几个重要的点,首先就是@autoreleasepool的使用,其实这里不添加这个autoreleasepool同样会自动释放内存,但添加后在这个代码块结束后就会立即释放,不会占用太多内存。其次,对于磁盘写入的操作是通过一个指定的串行队列实现的,这样不管执行多少个磁盘存储的操作,都必须一个一个的存储,这样就可以不用编写加锁的操作,可能有读者会疑惑为什么要进行加锁,因为并发情况下这些存储操作都不是线程安全的,很有可能会把路径修改掉或者产生其他异常行为,但使用了串行队列就完全不需要考虑加锁释放锁,一张图片存储完成才可以进行下一张图片存储的操作。

    以上参考了以下文章:
    iOS缓存 NSCache详解及SDWebImage缓存策略源码分析
    SDWebImage 源码阅读笔记
    SDWebImage实现分析
    【iOS开源库】SDWebImage源码阅读&原理解析

    相关文章

      网友评论

        本文标题:SDWebImage原理

        本文链接:https://www.haomeiwen.com/subject/okmnwftx.html