读完这篇文章你可以自己写一个 轻量级别的SDWebImage神器,这篇文章类似源码解析。但不同的是,不仅仅是解析,会带你手把手撸一个精简版的SDWebImage,更深刻的理解SDWebImage的架构和一些核心类的功能。学习一个东西必须要总结一次才能更加理解其中的精华,否则即便是读完了源码也学不到多少核心的动心。好记性不如烂笔头。
注:SDWebImage有很多功能,我这里就实现了一个核心的功能,多图异步下载、内存缓存、磁盘缓存。
轻量级仿SDWebImage源码下载链接:https://github.com/ZhengYaWei1992/ZWWebImageCache
就直接从功能实现开始,仿SDWebImage架构手把手撸一个轻量级SDWebImage,之后在针对SDWebImage框架深入分析,因为前期的实现都是模仿SDWebImage的实现,所以当你看懂前面的部分后,再去理解SDWebImage就是太轻而易举的事了。
一、轻量级SDWebImage的实现
1.1 、仿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中有四个核心的类,以及一些分类。
四大核心类以及其关系:
SDWebImageDownloader
、SDWebImageDownloaderOperation
、SDWebImageManager
、SDImageCache
核心分类:
UIView+WebCacheOperation
:主要在这个类中处理操作
UIButton+WebCache
:button上图片缓存
UIImage+GIF
: gif图片显示
UIImageView+WebCach
e:imageView上的图片缓存
NSData+ImageContentType
:获取文件类型
类的包含关系:
UIImageView+WebCache
中包含SDWebImageManager
和UIView+WebCacheOperation
,
SDWebImageManager
包含SDWebImageDownloader
SDWebImageManager
包含SDImageCache
SDWebImageDownloader
包含SDWebImageDownloaderOperation
。
其中SDWebImageDownloader
是负责下载的类。SDWebImageDownloaderOperation
是下载操作类,继承于NSOperation。SDWebImageDownloader
中包含SDWebImageDownloaderOperation
的头文件,前者依赖于后者。
SDImageCache
主要用于缓存处理,缓存处理同样是分为内存缓存和磁盘缓存,并定义了 SDImageCacheType
枚举用于区分缓存类型。
SDWebImageManager
中主要包含了SDWebImageDownloader
和SDImageCache
,并且还有一个创建单例的方法。这个类是一个核心的管理类,将缓存和下载的业务逻辑统一在一起,和我们实现的轻量级的图片缓存不同的是,我们将缓存的业务逻辑直接放置到管理类中,并没有单独抽取出来。
UIImageView+WebCache
分类中包含SDWebImageManager
和UIView+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的重复几率是很小的。