美文网首页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的重复几率是很小的。

相关文章

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

    读完这篇文章你可以自己写一个 轻量级别的SDWebImage神器,这篇文章类似源码解析。但不同的是,不仅仅是解析,...

  • SDWebImage

    1.SDWebImage源码解析(1)——总体架构,Cache读取2.SDWebImage源码解析(2)——ima...

  • SDWebImage源码解析(三)

    在前面的SDWebImage源码解析(一)和SDWebImage源码解析(二)中,解析了开源异步图片下载库SDWe...

  • SDWebImage源码解析(二)

    在SDWebImage源码解析(一)中,我从宏观上介绍了SDWebImage项目,并详细介绍了UIImageVie...

  • SDWebImage源码解析<二>

    前言 我们在第一篇文章《SDWebImage源码解析<一>》已经了解到SDWebImage是通过 SDWebIma...

  • IOS之SDWebImage(附Demo)

    目录 一、SDWebImage安装集成 二、SDWebImage原理说明 三、SDWebImage源码实现步骤 四...

  • SDWebImage 源码分析

    SDWebImage 源码分析 首先我 fork 了 SDWebImage 的源码,见 conintet/SDWe...

  • SDWebImage源码详解 - 异步下载器SDWebImage

    SDWebImage源码详解 - 异步下载器SDWebImageDownloader SDWebImage的图片下...

  • SDWebImage源码解析

    SDWebImage是一个开源的第三方库,支持从远程服务器下载并缓存图片的功能。它具有以下功能: 提供UIImag...

  • SDWebImage源码解析

    概览 说到 iOS界的图片加载库,SDWebImage可谓无人不知,其简介的接口以及异步下载与缓存的强大功能,深受...

网友评论

  • 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