现在很多APP在tableView加载图片的时候会给图片一个加载的效果这是我做的DEMO看一下效果
首先来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要作用就是:
Asynchronous image downloader with cache support with an UIImageView category.
一个异步下载图片并且支持缓存的 UIImageView
分类.
就这么直译过来相信各位也能理解, 框架中最最常用的方法其实就是这个:
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
当然这个框架中还有 UIButton
的分类, 可以给 UIButton
异步加载图片, 不过这个并没有 UIImageView
分类中的这个方法常用.
这个框架的设计还是极其的优雅和简洁, 主要的功能就是这么一行代码, 而其中复杂的实现细节全部隐藏在这行代码之后, 正应了那句话:
把简洁留给别人, 把复杂留给自己.
我们已经看到了这个框架简洁的接口, 接下来我们看一下 SDWebImage
是用什么样的方式优雅地实现异步加载图片和缓存的功能呢?
接下来我们就以 UIImageView+WebCache
中的
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder;
这一方法为入口研究一下 SDWebImage
是怎样工作的. 我们打开上面这段方法的实现代码 UIImageView+WebCache.m
当然你也可以 git clone git@github.com:rs/SDWebImage.git
到本地来查看.
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url
placeholderImage:placeholder
options:0
progress:nil
completed:nil];
}
这段方法唯一的作用就是调用了另一个方法
[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
在这个文件中, 你会看到很多的 sd_setImageWithURL......
方法, 它们最终都会调用上面这个方法, 只是根据需要传入不同的参数, 这在很多的开源项目中乃至我们平时写的项目中都是很常见的. 而这个方法也是 UIImageView+WebCache
中的核心方法.
这里就不再复制出这个方法的全部实现了.
操作的管理
这是这个方法的第一行代码:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #1
[self sd_cancelCurrentImageLoad];
这行看似简单的代码最开始是被我忽略的, 我后来才发现蕴藏在这行代码之后的思想, 也就是 SDWebImage
管理操作的办法.
框架中的所有操作实际上都是通过一个 operationDictionary
来管理, 而这个字典实际上是动态的添加到 UIView
上的一个属性, 至于为什么添加到 UIView
上, 主要是因为这个 operationDictionary
需要在 UIButton
和 UIImageView
上重用, 所以需要添加到它们的根类上.
这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:
// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
而这个方法会使当前 UIImageView
中的所有操作都被 cancel
. 不会影响之后进行的下载操作.
占位图的实现
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #4
if (!(options & SDWebImageDelayPlaceholder)) {
self.image = placeholder;
}
如果传入的 options
中没有 SDWebImageDelayPlaceholder
(默认情况下 options == 0
), 那么就会为 UIImageView
添加一个临时的 image
, 也就是占位图.
获取图片
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #8
if (url)
接下来会检测传入的 url
是否非空, 如果非空那么一个全局的 SDWebImageManager
就会调用以下的方法获取图片:
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
下载完成后会调用 (SDWebImageCompletionWithFinishedBlock)completedBlock
为 UIImageView.image
赋值, 添加上最终所需要的图片.
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #10
dispatch_main_sync_safe(^{
if (!wself) return;
if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
dispatch_main_sync_safe 宏定义
上述代码中的 dispatch_main_sync_safe
是一个宏定义, 点进去一看发现宏是这样定义的
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
相信这个宏的名字已经讲他的作用解释的很清楚了: 因为图像的绘制只能在主线程完成, 所以, dispatch_main_sync_safe
就是为了保证 block
能在主线程中执行.
而最后, 在 [SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
返回 operation
的同时, 也会向 operationDictionary
中添加一个键值对, 来表示操作的正在进行:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
它将 opertion
存储到 operationDictionary
中方便以后的 cancel
.
到此为止我们已经对 SDWebImage
框架中的这一方法分析完了, 接下来我们将要分析 SDWebImageManager
中的方法
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
SDWebImageManager
在 SDWebImageManager.h 中你可以看到关于 SDWebImageManager
的描述:
The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.
这个类就是隐藏在 UIImageView+WebCache
背后, 用于处理异步下载和图片缓存的类, 当然你也可以直接使用 SDWebImageManager
的上述方法 downloadImageWithURL:options:progress:completed:
来直接下载图片.
可以看到, 这个类的主要作用就是为 UIImageView+WebCache
和 SDWebImageDownloader, SDImageCache
之间构建一个桥梁, 使它们能够更好的协同工作, 我们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.
// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #6
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}SDWebImageCompletionBlock
这块代码的功能是确定 url
是否被正确传入, 如果传入参数的是 NSString
类型就会被转换为 NSURL
. 如果转换失败, 那么 url
会被赋值为空, 这个下载的操作就会出错.
回归正题,想要做出这个效果,无非是调用了SDWebImage的两个方法
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
在completedBlock中我们完成对图片View的加载
cell.image.alpha = 0.0;
[UIView transitionWithView:cell.image
duration:1.0
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
[cell.image setImage:image];
cell.image.alpha = 1.0;
} completion:NULL];
}
写到这里我们可以达到了渐隐渐现的效果了,但是发现了一个问题
我们会发现图片无论是上拉还是下滑所有的图片都会重新都会再次加载那我怎样让以前加载过的图片不让他加载动画呢,所以就要用SDWebImage读取缓存的方法
SDWebImageCache
SDWebImageCache.h 这个类在源代码中有这样的注释:
SDImageCache maintains a memory cache and an optional disk cache.
它维护了一个内存缓存和一个可选的磁盘缓存, 我们先来看一下在上一阶段中没有解读的两个方法, 首先是:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key
done:(SDWebImageQueryCompletedBlock)doneBlock;
这个方法的主要功能是异步的查询图片缓存. 因为图片的缓存可能在两个地方, 而该方法首先会在内存中查找是否有图片的缓存.
// SDWebImageCache
// queryDiskCacheForKey:done: #9
UIImage *image = [self imageFromMemoryCacheForKey:key];
这个 imageFromMemoryCacheForKey
方法会在 SDWebImageCache
维护的缓存 memCache
中查找是否有对应的数据, 而 memCache
就是一个 NSCache
.
如果在内存中并没有找到图片的缓存的话, 就需要在磁盘中寻找了, 这个就比较麻烦了..
在这里会调用一个方法 diskImageForKey
这个方法的具体实现我在这里就不介绍了, 涉及到很多底层 Core Foundation
框架的知识, 不过这里文件名字的存储使用 MD5
处理过后的文件名.
// SDImageCache
// cachedFileNameForKey: #6
CC_MD5(str, (CC_LONG)strlen(str), r);
对于其它的实现细节也就不多说了...
如果在磁盘中查找到对应的图片, 我们会将它复制到内存中, 以便下次的使用.
// SDImageCache
// queryDiskCacheForKey:done: #24
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
这些就是 SDImageCache
的核心内容了.所以我就用了判断当前是否储存过已经加载过的图片,就不让他加载的动画
- (BOOL)diskImageExistsForURL:(NSURL *)url {
NSString *key = [self cacheKeyForURL:url];
return [self.imageCache diskImageExistsWithKey:key];
}
最后由衷感谢draveness深入解析 iOS 开源项目
https://github.com/Draveness/iOS-Source-Code-Analyze,
最后Demo我会上传到github,欢迎互相交流和学习https://github.com/Jetsond/ZCDemo