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

读完这篇文章你可以自己写一个 轻量级别的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的重复几率是很小的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容