美文网首页iOS知识收集收藏iosiOS开发
SDWebImage源码解析及轻量级SDWebImage复现(附

SDWebImage源码解析及轻量级SDWebImage复现(附

作者: ZhengYaWei | 来源:发表于2017-03-11 17:43 被阅读1873次

    读完这篇文章你可以自己写一个 轻量级别的SDWebImage神器,这篇文章类似源码解析。但不同的是,不仅仅是解析,会带你手把手撸一个精简版的SDWebImage,更深刻的理解SDWebImage的架构和一些核心类的功能。学习一个东西必须要总结一次才能更加理解其中的精华,否则即便是读完了源码也学不到多少核心的动心。好记性不如烂笔头。

    注:SDWebImage有很多功能,我这里就实现了一个核心的功能,多图异步下载、内存缓存、磁盘缓存。

    轻量级仿SDWebImage源码下载链接:https://github.com/ZhengYaWei1992/ZWWebImageCache

    就直接从功能实现开始,仿SDWebImage架构手把手撸一个轻量级SDWebImage,之后在针对SDWebImage框架深入分析,因为前期的实现都是模仿SDWebImage的实现,所以当你看懂前面的部分后,再去理解SDWebImage就是太轻而易举的事了。

    一、轻量级SDWebImage的实现

    1.1 、仿SDWebImage的架构设计思路

    先总的看一下架构设计思路,如下图:

    仿SDWebImage的架构设计思路

    这个图可以先从Controller看起,Controller主要是否则调用UIImageView扥类中的方法,相信大家都知道SDWebImage最基本的设置图像的方法,需要导入UIImageView+webCache这个分类,这个架构同样是采用这种方法,通过UIImageView的分类设置图像,只用简单的传入一个urlString即可,当然占位图是必然支持的。

    下载操作类:

    UIImageView分类中包含下载操作管理类,下载操作管理类被设计为一个单例对象,它是这个架构中的核心,缓存、下载操作都是由它同意进行管理。因为缓存类本身涉及内容不是很多,所以这里就没有给缓存类单独抽离开来。

    下载操作类:

    首先要说明一下,图片的异步下载,这里主要是通过NSOperationQueue这个类实现的。所谓的下载类就是NSOperation,每一个操作都对应一个实例对象。下载操作类的实现,这里主要是自定义一个类继承自NSOperation,自定义下载操作。 它同样是由下载操作管理类进行管理。
    #######关于缓存:
    SDWebImage的缓存形式实际上是包含内存缓存和磁盘缓存。其中内存缓存主要是借助NSCache这个类实现的,磁盘缓存就是常规的文件读取啦。同样这个轻量级的SDWebImage内存缓存也是借助NSCache这个类实现的。关于NSCache这个类,可能日常开发中用的不是很普遍,但使用起来还是很简单的,基本使用形式和字典类似,但是又有很多和字典不同之处。这篇文章中会说一些NSCache的使用和注意事项。

    1.2 下载操作类的实现

    下载操作类实际是一个继承与 NSOperation的自定义类,该类中主要有两个核心方法:初始化对象和系统方法main。说明:main方法是系统方法,在操作添加到队列的时候会调用此方法。对外提供了两个属性图片的urlString以及下载完成的回调(主要是在main方法中实现)。
    操作初始化方法

    + (instancetype)downloaderOperationWithURLString:(NSString *)urlString finishedBlock:(void (^)(UIImage *image))finishedBlock{
        ZWDownloadOperation *op = [[ZWDownloadOperation alloc]init];
        op.urlString = urlString;
        op.finishedBlock = finishedBlock;
        return op;
    }
    

    重写系统main方法,当外部将该类的实例对象添加到队列中时,会调用mian方法。main方法中之所以会出现自动autoreleasepool,主要是因为异步操作无法访问主线程的自动释放池,所以要手动自己添加释放池。调用此方法会将读取的图片缓到磁盘中,下载完成后,会回到主线程产生回调,并返回UIImage对象。

    //重写main方法  操作添加到队列的时候会调用该方法
    - (void)main{
        //创建自动释放池:因如果是异步操作,无法访问主线程的自动释放池
        @autoreleasepool {
            //断言
            //添加断言后,if (self.finishedBlock) 不用再设置,如果为空了,程序会崩溃,同时会提醒:finishedBlock不能为空
            NSAssert(self.finishedBlock != nil, @"finishedBlock不能为空");
            
            //下载网络图片
            NSURL *url = [NSURL URLWithString:self.urlString];
            NSData *data = [NSData dataWithContentsOfURL:url];
            //缓存到沙盒中
            if (data) {
                [data writeToFile:[self.urlString appendCacheDir] atomically:YES];
            }
            //这里是子线程
            //NSLog(@"下载图片 %@ %@",self.urlString,[NSThread currentThread]);
            NSLog(@"从网络下载图片");
    
            //判断�操作是否被取消
            //如果取消,直接return。放在耗时操作之后和合理一些,取消操作的时候,不会拦截耗时操作,耗时操作依然可以执行。下次想显示图像的时候,耗时操作也执行完毕
            if (self.isCancelled) {
                return;
            }
            //图片下载完成回到主线程更新UI  通过使用断言,这里就不用使用if (self.finishedBlock) 判断了
            //if (self.finishedBlock) {
                [[NSOperationQueue mainQueue]addOperationWithBlock:^{
                    UIImage *img = [UIImage imageWithData:data];
                    self.finishedBlock(img);
                }];
            //}
        }
    }
    

    1.3 下载操作管理类

    毫无疑问,这个类必然是单例对象,管理类吗,当然要全局管理,有足够高的权限才能够被称为管理者。这个类主要是对完提供了三个方法:1、创建单例对象 2、开启下载任务 3、取消操作,因为要考虑到重复下载的情况,所以要对外提供这样一个接口。

    说是下载操作管理类,实际上是有点不合适的,应为该类主要用有两个功能:管理全局下载和管理全局缓存。全局缓存没有单独抽离出来,暂时就称为下载操作管理类就行,缓存会单独讲解一些的。实际SDWebImage的缓存功能室单独抽取出来的。

    下载操作管理类主要提供了这样三个属性。分别是全局队列、下载操作缓存池、图片内存缓存池。之所以会有下载操作缓存池,是因为要记录下载操作,如果下载操作已经存在就不用再去执行下载方法,直接reture,避免重复下载这种情况的出现。开始下载图片的时候,将草案做添加到操作缓存翅中。图片下载完成后,操作要从操作缓存池中移除。

    //全局队列
    @property(nonatomic,strong)NSOperationQueue *queue;
    //下载操作缓存池   这里不能改为NSCache,因为收到内存警告后NSCache移除所有对象,之后NSCache中就无法继续添加数据了
    @property(nonatomic,strong)NSMutableDictionary *operationCache;
    //图片缓存池(内存缓存)  从字典改为NSCache
    @property(nonatomic,strong)NSCache *imageCache;
    
    下载方法的实现。

    总的思路是这样的,先判断下载操作是否存在,如存在直接返回,避免重复下载。之后根据图片地址urlString判断是否存在内存缓存和磁盘缓存,如果存在直接调用回调,如过不存在就创建操作对象,添加到全局队列,开启下载任务。

    - (void)downloadWithURLString:(NSString *)urlString finishedBlock:(void (^)(UIImage *image))finishedBlock{
        //断言
        NSAssert(finishedBlock != nil, @"finishedBlock不能为空");
        //如果下载操作已经存在,直接返回。避免重复下载
        if (self.operationCache[urlString]) {
            return;
        }
        //判断图片是否有缓存(内存和磁盘缓存)
        if ([self checkImageCache:urlString]) {
            //如果有缓存,就要回调设置图像
            finishedBlock([self.imageCache objectForKey:urlString]);
            return;
        }
        
        ZWDownloadOperation *op = [ZWDownloadOperation downloaderOperationWithURLString:urlString finishedBlock:^(UIImage *image) {
            //回调
            finishedBlock(image);
            
            //缓存图片(内存缓存)
            //self.imageCache[urlString] = image;
            [self.imageCache setObject:image forKey:urlString];
            //下载完成,移除缓存的操作
            [self.operationCache removeObjectForKey:urlString];
        }];
        [self.queue addOperation:op];
        //缓存下载操作
        self.operationCache[urlString] = op;
    }
    
    关于取消操作。

    取消操作中药判断urlString是否为空,如果不做此判断,当urlString为nil的时候,执行 [self.operationCache removeObjectForKey:urlString];会发生崩溃。

    //取消操作
    - (void)cancelOperation:(NSString *)urlString{
        //避免第一次urlString为空,然后调用[self.operationCache removeObjectForKey:urlString]导致崩溃的问题
        if (urlString == nil) {
            return;
        }
        [self.operationCache[urlString] cancel];
        //从缓存池移除操作
        [self.operationCache removeObjectForKey:urlString];
    }
    
    关于缓存。

    对于缓存要明确明白分为内存缓存和磁盘还盘,在调用该类执行下载操作的时候,要首先判断是否有缓存。有缓存就回调缓存图片,无缓存就执行下载。但是内存缓存和磁盘缓存也是有一些注意的地方,判断是否有缓存应该判断是否有内存缓存,如果有直接回调;如果没再去判断是否有磁盘缓存。如果有磁盘缓存,直接回调,并将磁盘缓存图像添加到图像缓存中,下次再去判断这张图片的时候就可以从内存缓存池中读取。如果磁盘没有缓存,最后在开启下载图片任务。

    //检查是否有缓存(内存缓存和磁盘缓存)
    - (BOOL) checkImageCache:(NSString *)urlString{
        //1、检查内存缓存
        if ([self.imageCache objectForKey:urlString]) {
            NSLog(@"从内存中加载");
            return YES;
        }
        
        //2、检查沙盒缓存
        UIImage *img = [UIImage imageWithContentsOfFile:[urlString appendCacheDir]];
        //NSLog(@"沙盒路径:%@",[urlString appendCacheDir]);
        if (img) {
            NSLog(@"从沙盒中加载 ");
            //如果沙盒有图片,要保存到内存中============
            //self.imageCache[urlString] = img;
            [self.imageCache setObject:img forKey:urlString];
            return YES;
        }
        return NO;
    }
    
    关于NSCache

    NSCache使用起来基本和字典雷士,但是有一些注意点,同事功能比字典强大,因为可以设置缓存限额,当超过限额的时候,会自动移除之前的记录,然后添加新的记录。基本使用就是四句代码。但是对于移除所有数据有一点值得注意的,通常在使用NSCache的时候可以在didReceiveMemoryWarning收到内存警告方法中调用[self.cache removeAllObjects];这句代码。调用removeAllObjects之后,就无法再次往cache中缓存数据。但是如果不是在收到内存警告中removeAllObjects,依然是可以正常添加数据的。实际开发中应重视到这一点。

    //设置数据限额
    _cache.countLimit = 5;
    //添加或替换数据
     [self.cache setObject:@"sss" forKey:@"a"];
    //根据key获取数据
    [self.cache objectForKey:@"a"];
    //移除所有数据
    [self.cache removeAllObjects];
    

    1.4 关于UIImageView的分类实现

    分类中主要有一个核心方法,设置UIImageView的图片。直接一行代码调用。这里同样考虑到一点就是频繁改变UIImageView的图片。假设在控制器的touchBegan方法中每次点击都会改变imageView的图片,点击多少次图片就会连续切换多少次。但是加入不想让图片连续切换,只要显示第一张图片和最后一张图片即可,其他中间触发时间的图片就不要显示了,并且取消下载任务。为了满足这个需要,所以要借助运行时的关联对象增加属性,记录当前图片的urlString地址。除了这些额外的处理外,核心就是调用操作管理类中的获取图片的方法。实现代码如下。

    - (void)zw_setImageWithUrlString:(NSString *)urlString{
        //防止连续设置图片,UIImageView上的图片频繁切换
        //判断当前点击的图片地址和上一次图片的地址是否一样,如果不一样取消上一次操作
        if (![urlString isEqualToString:self.currentURLString]) {
            //取消上一次操作
            //[self.operationCache[self.currentURLString] cancel];
            [[ZWDownloderOperationManager sharedManager]cancelOperation:self.currentURLString];
        }
        //记录上一次的图片地址
        self.currentURLString = urlString;
        //下载图片
        [[ZWDownloderOperationManager sharedManager]downloadWithURLString:urlString finishedBlock:^(UIImage *image) {
            self.image = image;
        }];
    }
    

    关联对象扩充属性。

    - (void)setCurrentURLString:(NSString *)currentURLString{
        objc_setAssociatedObject(self, @"currentURLString", currentURLString, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    - (NSString *)currentURLString{
        return  objc_getAssociatedObject(self, @"currentURLString");
    }
    

    当然还可以在此基础上扩展一个设置占位图的方法,代码如下。

    - (void)zw_setImageWithUrlString:(NSString *)urlString withPlaceHolderImageName:(NSString *)placeholderStr{
        self.image = [UIImage imageNamed:placeholderStr];
        [self zw_setImageWithUrlString:urlString];
    }
    
    见证成果的时刻

    好了,基本就这些代码,剩下的直接在控制器中调用UIImageView分类中的方法即可,直接上效果图啦。

    成果

    二、SDWebImage框架结构

    2.1 SDWebImage中有四个核心的类,以及一些分类。

    四大核心类以及其关系:

    SDWebImageDownloaderSDWebImageDownloaderOperationSDWebImageManagerSDImageCache

    核心分类:

    UIView+WebCacheOperation:主要在这个类中处理操作
    UIButton+WebCache:button上图片缓存
    UIImage+GIF: gif图片显示
    UIImageView+WebCache:imageView上的图片缓存
    NSData+ImageContentType:获取文件类型

    类的包含关系:

    UIImageView+WebCache中包含SDWebImageManagerUIView+WebCacheOperation
    SDWebImageManager包含SDWebImageDownloader
    SDWebImageManager包含SDImageCache
    SDWebImageDownloader包含SDWebImageDownloaderOperation

    其中SDWebImageDownloader是负责下载的类。SDWebImageDownloaderOperation是下载操作类,继承于NSOperation。SDWebImageDownloader中包含SDWebImageDownloaderOperation的头文件,前者依赖于后者。

    SDImageCache主要用于缓存处理,缓存处理同样是分为内存缓存和磁盘缓存,并定义了 SDImageCacheType枚举用于区分缓存类型。

    SDWebImageManager中主要包含了SDWebImageDownloaderSDImageCache,并且还有一个创建单例的方法。这个类是一个核心的管理类,将缓存和下载的业务逻辑统一在一起,和我们实现的轻量级的图片缓存不同的是,我们将缓存的业务逻辑直接放置到管理类中,并没有单独抽取出来。

    UIImageView+WebCache分类中包含SDWebImageManagerUIView+WebCacheOperation核心类。UIImageView+WebCache中有一个sd_cancelCurrentImageLoad方法,这个方法主要是在取消当前图片下载,防止重复下载操作。具体实现放在UIView+WebCacheOperation

    整的来说,和我们之前的实现还是很类似的。实际上我们实现的是模仿SDWebImage实现的一个简单的图片缓存处理。😀

    另外,SDWebImageDownloader在初始化initialize的时候添加了一些通知,主要用于监听下载任务,显示加载指示器。

    2.2 SDWebImage的缓存

    SDWebImage的缓存也是分为内存缓存和磁盘缓存:其中内存缓存同样主要是通过NSCache处理。

    磁盘缓存处理中会设置自动清理磁盘空间,清理周期设置为一周。SDWebImageCache中有这样一个属性,@property (assign, nonatomic) NSInteger maxCacheAge;该属性默认值是一周(kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7)设置清理磁盘缓存的周期。

    处理方式,请看SDWebCache中的- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock 方法。这个方法中首先看到的是异步操作,因为处理文件较多时,比较消耗资源最好是异步的方式处理。下面的代码是定时清理磁盘缓存方法中的部分代码,思路是获取到一周前的时间,再通过NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]获取文件的时间,比较两个是时间,如果大于一周的时间,就将文件路径添加到urlsToDelete待删除数组中,最后遍历这个数组,统一删除过期资源。

    //获取一周前的时间
    NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
    NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
    //用于记录当前文件总大小
    NSUInteger currentCacheSize = 0;
    
    NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
       NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
    
       if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
           continue;
       }
    
       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];
     }
    //删除超过一周时间的文件
    for (NSURL *fileURL in urlsToDelete) {
          [_fileManager removeItemAtURL:fileURL error:nil];
    }
    

    2.3 关于SDWebImage的几个小问题:

    1、最大并发数多少?

    SDWebImageDownloader.m文件中的init方法中有这样一行代码。即最大线程并发数为6,实际开发中并不是开启的线程越多越好,当线程过多的时候也会影响性能,一般建议线程不要超过8前后。

    _downloadQueue.maxConcurrentOperationCount = 6;
    
    2、是否支持gif?

    支持gif,主要是在UIImage+gif这个分类中。这个类中总共就只有三个方法。

    self.imageView.image = [UIImage sd_animatedGIFNamed:@"1.gif"];
    

    按照如上方式设置的gif图片,在一些情况可能无法正常显示gif图片,这个是新版本SDWebImage的bug,老版本中不存在这样的问题。设置gif图片最好实时通过下面这个方法。

     self.imageView.image = [UIImage sd_animatedGIFNamed:@"1.gif"];
     NSString  *filePath = [[NSBundle bundleWithPath:[[NSBundle mainBundle] bundlePath]] pathForResource:@"1.gif" ofType:nil];
     NSData  *imageData = [NSData dataWithContentsOfFile:filePath];
     self.imageView.image = [UIImage sd_animatedGIFWithData:imageData];
    
    3、如何判断文件的类型?

    NSData+ImageContentType.m中,sd_contentTypeForImageData方法可以获取到文件的类型。其中 [data getBytes:&c length:1];是获取文件的第一个字节,文件的第一个字节中包含文件类型相关的信息。

    + (NSString *)sd_contentTypeForImageData:(NSData *)data {
        uint8_t c;
        [data getBytes:&c length:1];
        switch (c) {
            case 0xFF:
                  return @"image/jpeg";
           case 0x89:
                return @"image/png";
           case 0x47:
                 return @"image/gif";
          case 0x49:
          case 0x4D:
                return @"image/tiff";
          case 0x52:
             // R as RIFF for WEBP
               if ([data length] < 12) {
                      return nil;
               }
    
              NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
             if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                  return @"image/webp";
             }
    
            return nil;
        }
        return nil;
    }
    
    
    4、磁盘缓存文件名称是什么?

    通过命名空间com.hackemist.SDWebImageCache.隔离区分。
    为了防止图片名冲突,根据MD5计算。md5的重复几率是很小的。

    相关文章

      网友评论

      • 2ac712b8d073:有个地方不是很清楚。留个联系方式交流下吧
        ZhengYaWei:@啦啦啦德玛西亚万岁啊 1214729173 QQ号
      • 那夜孤舟:cell的重用和异步请求的冲突是怎么解决的,这个类会不会在cell特别多的时候,快速滑动的时候出现图片错乱的问题,我没见你解决这个问题的代码
      • crazyfox:厉害,准备照着写一个:relaxed:
      • c44c0bf3f747:这东西研究研究确实挺好的 回头也来研究一下
      • HustBroventure:下载的操作如果能换成nsurlsession就好了。
        ZhengYaWei:@HustBroventure 这几天打算写个基于nsurlsession的多任务下载
      • 27205419bb06:6666
        2ac712b8d073:@ZhengYaWei :smile:
        ZhengYaWei:@德玛西亚德玛西亚 :smile:

      本文标题:SDWebImage源码解析及轻量级SDWebImage复现(附

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