以下所有的介绍不想看源码,可以直接看文字介绍,一样的逻辑,不妨碍阅读
首先,所有的源码和作者提供的基本资料在这里都能找到点击打开链接
YYWebImage是网络图片下载的Category,其中YYImage是编码解码的基石,YYImage已经单独拉出一篇分析过了
YYImage分析,非常重要的编码解码思路,可以看看,还有一个就是YYCache,这里就和YYWebImage一起分析了。大家熟知的就是SDWebImage,该库的思路也已经分析过了,最新的版本和以前旧版本都有分析 SDWebImage源码分析,YYWebImage和SDWebImage一般情况下都能很好的使用,两者的实现思路大致一直,但是细节和性能上还是有差异的,SDWebImage很明显有个GIF显示的问题,这个问题他自己在源码中也有写出,这个问题我们最后来分析,到底是什么缺陷。老套路,跟着YYWebimage网络下载图片的流程走一遍源码,把所有的知识点都打通。
[_webImageView yy_setImageWithURL:url
placeholder:nil
options:YYWebImageOptionProgressiveBlur | YYWebImageOptionShowNetworkActivity | YYWebImageOptionSetImageWithFadeAnimation
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
if (expectedSize > 0 && receivedSize > 0) {
CGFloat progress = (CGFloat)receivedSize / expectedSize;
progress = progress < 0 ? 0 : progress > 1 ? 1 : progress;
if (_self.progressLayer.hidden) _self.progressLayer.hidden = NO;
_self.progressLayer.strokeEnd = progress;
}
}
transform:nil
completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
if (stage == YYWebImageStageFinished) {
_self.progressLayer.hidden = YES;
[_self.indicator stopAnimating];
_self.indicator.hidden = YES;
if (!image) _self.label.hidden = NO;
}
}];
这个方法看起来有点长,不想看注释的可以跳过,下面给你来个精简版本的 这东西展开来好多东西,开始吧
// 核心API
- (void)yy_setImageWithURL:(NSURL *)imageURL
placeholder:(UIImage *)placeholder
options:(YYWebImageOptions)options
manager:(YYWebImageManager *)manager
progress:(YYWebImageProgressBlock)progress
transform:(YYWebImageTransformBlock)transform
completion:(YYWebImageCompletionBlock)completion {
if ([imageURL isKindOfClass:[NSString class]]) imageURL = [NSURL URLWithString:(id)imageURL];
// 生成管理类单例
manager = manager ? manager : [YYWebImageManager sharedManager];
// 私有类
_YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageSetterKey);
// 通过Category挂载 _YYWebImageSetterKey
if (!setter) {
setter = [_YYWebImageSetter new];
objc_setAssociatedObject(self, &_YYWebImageSetterKey, setter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// 取消之前的未现在完成任务 全局计数递增
int32_t sentinel = [setter cancelWithNewURL:imageURL];
// 永远保证主线程回调Block inline 方法
_yy_dispatch_sync_on_main_queue(^{
if ((options & YYWebImageOptionSetImageWithFadeAnimation) &&
!(options & YYWebImageOptionAvoidSetImage)) {
if (!self.highlighted) {
[self.layer removeAnimationForKey:_YYWebImageFadeAnimationKey];
}
}
if (!imageURL) {
if (!(options & YYWebImageOptionIgnorePlaceHolder)) {
self.image = placeholder;
}
return;
}
// get the image from memory as quickly as possible
// 通过内存YYMemory缓存拿 尽快拿,这里内存是用自制链表链接
UIImage *imageFromMemory = nil;
if (manager.cache &&
!(options & YYWebImageOptionUseNSURLCache) &&
!(options & YYWebImageOptionRefreshImageCache)) {
imageFromMemory = [manager.cache getImageForKey:[manager cacheKeyForURL:imageURL] withType:YYImageCacheTypeMemory];
}
// 拿到返回
if (imageFromMemory) {
if (!(options & YYWebImageOptionAvoidSetImage)) {
self.image = imageFromMemory;
}
if(completion) completion(imageFromMemory, imageURL, YYWebImageFromMemoryCacheFast, YYWebImageStageFinished, nil);
return;
}
if (!(options & YYWebImageOptionIgnorePlaceHolder)) {
self.image = placeholder;
}
__weak typeof(self) _self = self;
dispatch_async([_YYWebImageSetter setterQueue], ^{
// Progress 任务
YYWebImageProgressBlock _progress = nil;
if (progress) _progress = ^(NSInteger receivedSize, NSInteger expectedSize) {
dispatch_async(dispatch_get_main_queue(), ^{
progress(receivedSize, expectedSize);
});
};
// 完成任务
__block int32_t newSentinel = 0;
__block __weak typeof(setter) weakSetter = nil;
YYWebImageCompletionBlock _completion = ^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
// _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
__strong typeof(_self) self = _self;
BOOL setImage = (stage == YYWebImageStageFinished || stage == YYWebImageStageProgress) && image && !(options & YYWebImageOptionAvoidSetImage);
dispatch_async(dispatch_get_main_queue(), ^{
BOOL sentinelChanged = weakSetter && weakSetter.sentinel != newSentinel;
if (setImage && self && !sentinelChanged) {
BOOL showFade = ((options & YYWebImageOptionSetImageWithFadeAnimation) && !self.highlighted);
if (showFade) {
CATransition *transition = [CATransition animation];
transition.duration = stage == YYWebImageStageFinished ? _YYWebImageFadeTime : _YYWebImageProgressiveFadeTime;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade;
[self.layer addAnimation:transition forKey:_YYWebImageFadeAnimationKey];
}
// 超级核心代码 别看就这么点,这句话就是精髓 通过四步骤,GIF情况下让第一帧启动定时器,缓存策略解码缓存/预解码缓存进行播放
self.image = image;
}
if (completion) {
if (sentinelChanged) {
completion(nil, url, YYWebImageFromNone, YYWebImageStageCancelled, nil);
} else {
completion(image, url, from, stage, error);
}
}
});
};
// _YYWebImageSetter 私有类
newSentinel = [setter setOperationWithSentinel:sentinel url:imageURL options:options manager:manager progress:_progress transform:transform completion:_completion];
weakSetter = setter;
});
});
}
注: 这里保留一个如何查找缓存的介绍,等最后下载完成之后如何缓存进去的一起分析
/// Create new operation for web image and return a sentinel value.
/// 生成一个新的任务队列 下载webImage 返回全局计数递增
- (int32_t)setOperationWithSentinel:(int32_t)sentinel
url:(NSURL *)imageURL
options:(YYWebImageOptions)options
manager:(YYWebImageManager *)manager
progress:(YYWebImageProgressBlock)progress
transform:(YYWebImageTransformBlock)transform
completion:(YYWebImageCompletionBlock)completion {
// 例如取消的时候是10,那么进来的时候也应该是10,如果不同,说明有其他任务让计数器增加了,直接返回
if (sentinel != _sentinel) {
if (completion) completion(nil, imageURL, YYWebImageFromNone, YYWebImageStageCancelled, nil);
return _sentinel;
}
// 创建下载任务,而且马上开始
NSOperation *operation = [manager requestImageWithURL:imageURL options:options progress:progress transform:transform completion:completion];
if (!operation && completion) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"YYWebImageOperation create failed." };
completion(nil, imageURL, YYWebImageFromNone, YYWebImageStageFinished, [NSError errorWithDomain:@"com.ibireme.webimage" code:-1 userInfo:userInfo]);
}
// 加锁
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
// 相等 说明是同一个操作
if (sentinel == _sentinel) {
// 取消之前的下载操作
if (_operation) [_operation cancel];
// 把刚才生成的任务添加YYWebImageSetter字段里面
_operation = operation;
// 任务生成 计数+1
sentinel = OSAtomicIncrement32(&_sentinel);
} else {
[operation cancel];
}
dispatch_semaphore_signal(_lock);
return sentinel;
}
重写Start isFinished isExcuting isCanceled几个方法
- (void)start {
@autoreleasepool {
[_lock lock];
self.started = YES;
if ([self isCancelled]) {
[self performSelector:@selector(_cancelOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
self.finished = YES;
} else if ([self isReady] && ![self isFinished] && ![self isExecuting]) {
if (!_request) {
self.finished = YES;
if (_completion) {
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{NSLocalizedDescriptionKey:@"request in nil"}];
_completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageFinished, error);
}
} else {
// 任务开始
self.executing = YES;
// 后台线程 开启下载任务 NSURLConnection
[self performSelector:@selector(_startOperation) onThread:[[self class] _networkThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
if ((_options & YYWebImageOptionAllowBackgroundTask) && _YYSharedApplication()) {
__weak __typeof__ (self) _self = self;
if (_taskID == UIBackgroundTaskInvalid) {
_taskID = [_YYSharedApplication() beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (_self) self = _self;
if (self) {
[self cancel];
self.finished = YES;
}
}];
}
}
}
}
[_lock unlock];
}
}
/// Network thread entry point.
+ (void)_networkThreadMain:(id)object {
@autoreleasepool {
[[NSThread currentThread] setName:@"com.ibireme.webimage.request"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
/// Global image request network thread, used by NSURLConnection delegate.
+ (NSThread *)_networkThread {
static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];
if ([thread respondsToSelector:@selector(setQualityOfService:)]) {
thread.qualityOfService = NSQualityOfServiceBackground;
}
[thread start];
});
return thread;
}
1.开始任务,标记execting为YES,然后调用_startOperation,这里依然是AF那会儿线程保活,在异步线程中给Runloop add一个port消息源,激活线程跑起来,全局图片网络下载线程,用来NSURLConnection代理回调,这个问题NSURLSession已经维护了自己的线程,所以AF和SD都去掉了维护自己的线程,保活的操作,由NSURLSession自己来维护下载的线程,上面就是自己维护的全局图片下载线程,通过addport让线程Runloop跑起来
// runs on network thread
// 开启Operation任务
- (void)_startOperation {
if ([self isCancelled]) return;
@autoreleasepool {
// get image from cache
if (_cache &&
!(_options & YYWebImageOptionUseNSURLCache) &&
!(_options & YYWebImageOptionRefreshImageCache)) {
// 先从内存中拿 有就返回
UIImage *image = [_cache getImageForKey:_cacheKey withType:YYImageCacheTypeMemory];
if (image) {
[_lock lock];
if (![self isCancelled]) {
if (_completion) _completion(image, _request.URL, YYWebImageFromMemoryCache, YYWebImageStageFinished, nil);
}
[self _finish];
[_lock unlock];
return;
}
// 内存中没有,在Disk中拿
if (!(_options & YYWebImageOptionIgnoreDiskCache)) {
__weak typeof(self) _self = self;
dispatch_async([self.class _imageQueue], ^{
__strong typeof(_self) self = _self;
if (!self || [self isCancelled]) return;
UIImage *image = [self.cache getImageForKey:self.cacheKey withType:YYImageCacheTypeDisk];
// 拿到了,直接缓存到内存中
if (image) {
[self.cache setImage:image imageData:nil forKey:self.cacheKey withType:YYImageCacheTypeMemory];
[self performSelector:@selector(_didReceiveImageFromDiskCache:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
} else {
// 没拿到,网络下载
[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];
}
});
return;
}
}
}
[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];
}
// runs on network thread
// 开启网路下载请求
- (void)_startRequest:(id)object {
if ([self isCancelled]) return;
@autoreleasepool {
// 黑名单 返回
。。。
// 文件URL
。。。
// request image from web
// 网络请求
[_lock lock];
if (![self isCancelled]) {
// NSURLCOnnection 下载任务开启 代理回调
_connection = [[NSURLConnection alloc] initWithRequest:_request delegate:[_YYWebImageWeakProxy proxyWithTarget:self]];
if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) {
[YYWebImageManager incrementNetworkActivityCount];
}
}
[_lock unlock];
}
}
2.省略了部分代码,先从内存拿,再从磁盘拿,无论哪里有,拿到了都要二级缓存起来,没拿到再进行_startRequest进行网络下载,这里用的是NSURLConnection进行资源下载
/**
NSURLConnection回调代理
总之,这个代理是持续回调的,这里无论多少帧的图片,都是返回第一帧给外部先显示
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
@autoreleasepool {
[_lock lock];
BOOL canceled = [self isCancelled];
[_lock unlock];
if (canceled) return;
if (data) [_data appendData:data];
if (_progress) {
[_lock lock];
if (![self isCancelled]) {
_progress(_data.length, _expectedSize);
}
[_lock unlock];
}
/*--------------------------- progressive ----------------------------*/
// 解码器
if (!_progressiveDecoder) {
_progressiveDecoder = [[YYImageDecoder alloc] initWithScale:[UIScreen mainScreen].scale];
}
// 关键代码-----> 解码器每一帧的图像用frames数组保存 _YYImageDecoderFrame 每一帧的图像
[_progressiveDecoder updateData:_data final:NO];
if ([self isCancelled]) return;
// 核心代码------> 注意每一次回到的时候无论多少帧,都是返回第一帧给外部先显示用
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
if (frame.image) {
[_lock lock];
if (![self isCancelled]) {
_completion(frame.image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
_lastProgressiveDecodeTimestamp = now;
}
[_lock unlock];
}
return;
// 同上
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
if (!image) return;
if ([self isCancelled]) return;
if (!YYCGImageLastPixelFilled(image.CGImage)) return;
_progressiveDisplayCount++;
image = [image yy_imageByBlurRadius:radius tintColor:nil tintMode:0 saturation:1 maskImage:nil];
if (image) {
[_lock lock];
if (![self isCancelled]) {
_completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
_lastProgressiveDecodeTimestamp = now;
}
[_lock unlock];
}
}
}
}
/**
代理数据传输完成 回调
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
@autoreleasepool {
[_lock lock];
_connection = nil;
if (![self isCancelled]) {
__weak typeof(self) _self = self;
dispatch_async([self.class _imageQueue], ^{
__strong typeof(_self) self = _self;
if (!self) return;
BOOL shouldDecode = (self.options & YYWebImageOptionIgnoreImageDecoding) == 0;
// 知识点 没有 YYWebImageOptionIgnoreAnimatedImage 就是allowAnimation = (0==0) YES
BOOL allowAnimation = (self.options & YYWebImageOptionIgnoreAnimatedImage) == 0;
UIImage *image;
BOOL hasAnimation = NO;
// 允许动画 多帧的图像初始化处理 上面的帧显示而已,不管是什么图片 finish的时候就不同了
if (allowAnimation) {
// 这里的data是所有接收完整的Image图像资源data 该方法自带生成解码器和所有帧
// YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
// YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
// UIImage *image = frame.image;
// 注意看上面,所有的数据data进去,生成返回的图片都是第一帧的 这里为什么要用YYImage调用,是多帧,需要和第一帧绑定Decoder解码器进行定时器播放的 重点,划重点
image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale];
// 依旧是第一帧解码
if (shouldDecode) image = [image yy_imageByDecoded];
if ([((YYImage *)image) animatedImageFrameCount] > 1) {
// 多帧 例如 GIF
hasAnimation = YES;
}
} else {
// 单帧的不需要调用上面的YYImage便利方法了,直接解码即可 上面多余的参数也是给GIF多帧图的
// 单帧就不需要绑定了,直接解码器解出第一帧即可
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale];
image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image;
}
/*
If the image has animation, save the original image data to disk cache. 动画,保存原始data到磁盘
If the image is not PNG or JPEG, re-encode the image to PNG or JPEG for 不是png或者jpeg,encode成这两个
better decoding performance.
这里如果image不属于自己的类型 清除data
*/
YYImageType imageType = YYImageDetectType((__bridge CFDataRef)self.data);
switch (imageType) {
case YYImageTypeJPEG:
case YYImageTypeGIF:
case YYImageTypePNG:
case YYImageTypeWebP: { // save to disk cache
if (!hasAnimation) {
if (imageType == YYImageTypeGIF ||
imageType == YYImageTypeWebP) {
self.data = nil; // clear the data, re-encode for disk cache
}
}
} break;
default: {
self.data = nil; // clear the data, re-encode for disk cache
} break;
}
if ([self isCancelled]) return;
// transfer Block 外部是否需要传新的图片替换下载下来的图片
if (self.transform && image) {
UIImage *newImage = self.transform(image, self.request.URL);
if (newImage != image) {
self.data = nil;
}
// 由外部赋值
image = newImage;
if ([self isCancelled]) return;
}
[self performSelector:@selector(_didReceiveImageFromWeb:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
});
if (![self.request.URL isFileURL] && (self.options & YYWebImageOptionShowNetworkActivity)) {
[YYWebImageManager decrementNetworkActivityCount];
}
}
[_lock unlock];
}
}
3.这里的介绍都是针对上面连接的代码的,当开始NSURLConnection的时候,这两个代理就是核心,一个是不断接受数据用的,另一个是接受完成数据之后所有数据的回调
_progressiveDecoder这个解码器又来了,如果想仔细了解如何解码图片的可以参考YYImage分析
data是慢慢拼接的,把data传进去给解码器把相关所有帧的数据都解出来,存储在解码器的frames里面,每个帧对应的帧图片都是未解码的,主要看下这两句代码
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
其中根据解码器,获取到第一帧的图片并返回,因此,无论下载到什么程度,获取到的image都是第一帧的静态图片,因此GIF为例,没下载完,都是显示第一帧图片在那里给用户看,而且这种调用方式,只是单纯获取到第一帧图片资源,而没有把第一帧Image图片资源关联对应的解码器,这里真的有点绕,但是我个人觉得要真的理解透,就要知道这两个方法的区别,因为YYImage的通过Data初始化,都是返回第一帧的图片使用的,因此如果是PNF或者JPEG,直接拿第一帧即可,无需其他操作,但是如果GIF为例,你拿到了第一帧,那你怎么拿到后面帧通过CADisplayLink进行帧播放?这里又是YYImage的只是,单独拿出来真的是很重要的,需要的朋友,先看了YYImage分析在来看就会明白很多,多帧和单帧是不同的操作,具体这里也有体现,看下面代码
// 允许动画 多帧的图像初始化处理 上面的帧显示而已,不管是什么图片 finish的时候就不同了
if (allowAnimation) {
// 这里的data是所有接收完整的Image图像资源data 该方法自带生成解码器和所有帧
// YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
// YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
// UIImage *image = frame.image;
// 注意看上面,所有的数据data进去,生成返回的图片都是第一帧的 这里为什么要用YYImage调用,是多帧,需要和第一帧绑定Decoder解码器进行定时器播放的 重点,划重点
image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale];
// 依旧是第一帧解码
if (shouldDecode) image = [image yy_imageByDecoded];
if ([((YYImage *)image) animatedImageFrameCount] > 1) {
// 多帧 例如 GIF
hasAnimation = YES;
}
} else {
// 单帧的不需要调用上面的YYImage便利方法了,直接解码即可 上面多余的参数也是给GIF多帧图的
// 单帧就不需要绑定了,直接解码器解出第一帧即可
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale];
image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image;
}
可以看到如果allowAnimation,就是多帧动图,这里的初始化方式是通过YYImage初始化的
// data 解码 存储
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
// 解码第一帧图像资源出来
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
// 第一帧图像返回
UIImage *image = frame.image;
if (!image) return nil;
// self 对象就是第一帧图像 UIImage 父类可以指向子类 UIImage = YYimage new 子类调用父类的,返回子类对象
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
这段代码是YYImage初始化的时候内部自带的解码器,把所有的帧都解出来,返回第一帧YYImage的方法,因此,通过这种方式初始化,YYImage第一帧是有个Decoder解码器属性的,所以后面的动画都可以根据这个YYImage对象带的解码器,逐帧解码显示出来,但是下面那种直接没有用YYImage包装,直接用YYImageDecoder解码器直接解码返回,然后通过index获取到第1帧的图像解码,这个时候是针对单帧图片的。握草,我打字都打累了,这个真的很关键啊,不然你无法理解为什么会这样。解码器播放动图那里会用到的。。。。敲黑板划重点啊有木有,咳咳咳
你妹。为了讲清楚,累死我了,喝个旺仔压压惊
明白了这个,你就能知道为什么didReceiveData代理方法里面如果是动态图GIF资源,还是只是显示第一帧,不会继续播放,因为他是直接用解码器解出来来的第一帧,而不是通过YYImage包装再解的,直接解就会让定时器执行的时候,解码器是空的,获取到就可以当做单帧图片显示,因此还没下载完之前就是静态一帧图片而已
// finish的代理方法那里调用该方法接收网络图片数据
- (void)_didReceiveImageFromWeb:(UIImage *)image {
@autoreleasepool {
[_lock lock];
if (![self isCancelled]) {
if (_cache) {
// 有图片 或者需要刷新缓存
if (image || (_options & YYWebImageOptionRefreshImageCache)) {
NSData *data = _data;
dispatch_async([YYWebImageOperation _imageQueue], ^{
// 判断缓存类型
YYImageCacheType cacheType = (_options & YYWebImageOptionIgnoreDiskCache) ? YYImageCacheTypeMemory : YYImageCacheTypeAll;
// 磁盘缓存 file + db
[_cache setImage:image imageData:data forKey:_cacheKey withType:cacheType];
});
}
}
_data = nil;
NSError *error = nil;
if (!image) {
error = [NSError errorWithDomain:@"com.ibireme.image" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Web image decode fail." }];
if (_options & YYWebImageOptionIgnoreFailedURL) {
if (URLBlackListContains(_request.URL)) {
error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{ NSLocalizedDescriptionKey : @"Failed to load URL, blacklisted." }];
} else {
URLInBlackListAdd(_request.URL);
}
}
}
if (_completion) _completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageFinished, error);
[self _finish];
}
[_lock unlock];
}
}
下面主要介绍下内存缓存以及磁盘缓存
// GIF WebP imageData是nil
- (void)setImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key withType:(YYImageCacheType)type {
if (!key || (image == nil && imageData.length == 0)) return;
__weak typeof(self) _self = self;
// 内存缓存
if (type & YYImageCacheTypeMemory) { // add to memory cache
if (image) {
// 该字段标识 是否图片资源可以直接展示到屏幕上而不需要任何解码操作 YES代表直接可以展示
if (image.yy_isDecodedForDisplay) {
// 不需要解码,直接缓存到内存中
[_memoryCache setObject:image forKey:key withCost:[_self imageCost:image]];
} else {
dispatch_async(YYImageCacheDecodeQueue(), ^{
__strong typeof(_self) self = _self;
if (!self) return;
// NO 代表不能展示 调用我们上一期将提到的图片解码,解码没有在后台,需要放到异步解码 递归锁 线程安全的
[self.memoryCache setObject:[image yy_imageByDecoded] forKey:key withCost:[self imageCost:image]];
});
}
} else if (imageData) {
dispatch_async(YYImageCacheDecodeQueue(), ^{
__strong typeof(_self) self = _self;
if (!self) return;
UIImage *newImage = [self imageFromData:imageData];
[self.memoryCache setObject:newImage forKey:key withCost:[self imageCost:newImage]];
});
}
}
// 磁盘花村
if (type & YYImageCacheTypeDisk) { // add to disk cache
// 有imgData的情况是有规定数据类型的
if (imageData) {
if (image) {
// 关联 扩展的Data
[YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:imageData];
}
[_diskCache setObject:imageData forKey:key];
} else if (image) {
// 没有data的情况,格式对,把图像转换成
// If the image is not PNG or JPEG, re-encode the image to PNG or JPEG for better decoding performance. 不是png或者jpeg,encode成这两个
dispatch_async(YYImageCacheIOQueue(), ^{
__strong typeof(_self) self = _self;
if (!self) return;
NSData *data = [image yy_imageDataRepresentation];
[YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:data];
[self.diskCache setObject:data forKey:key];
});
}
}
}
先看看YYMemoryCache的结构,其中里面有个YYLinkedMap双向链表,下面是双向链表和每个链表Node的结构
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly 链表存储实际key(url) 和 value(Node)
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly // 头
_YYLinkedMapNode *_tail; // LRU, do not change it directly 尾
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic 节点头指针
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic 节点尾指针
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
再来看看YYMemoryCache如何进行存取的
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
// key 存在 object没有的话 移除
if (!object) {
[self removeObjectForKey:key];
return;
}
// 加锁锁 YYLinkedMap中的字典中根据Key取Node
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
// 存在 替换
if (node) {
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
// 把节点移动到链表头部
[_lru bringNodeToHead:node];
} else {
// 不存在 第一次 赋值 创建一个新的
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
// 把节点插入到链表头部
[_lru insertNodeAtHead:node];
}
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
// 如果超过最大内存缓存 优先移除链表尾部节点 而且从链表对象的Dic中移除Key value
if (_lru->_totalCount > _countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}
- (id)objectForKey:(id)key {
if (!key) return nil;
pthread_mutex_lock(&_lock);
// 根据YYMemoryCache中的_YYLinkedMap *_lru;(可以理解为链表)链表中的字典 读取对应的节点
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
// 如果节点存在,就把节点移动到链表头部
if (node) {
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
先来看看存,首先pthread_mutex_init由于OSSPinLock自旋锁的问题,可以用信号量和pthread_mutex_init来追求性能的最优。
加锁通过YYMemoryCache字段NodeMap链表的dict根据对应的key去拿,有的话就替换,淘汰算法,(bringNodeToHead)把该Node拿到链表头部,如果没有,就创建一个新的Node,然后调用insertNodeAtHead插入到链表头部,这里注意的是,如果是插入操作,就需要在链表的dict字典中把对应的key和value(Node对象 里面包含url和data)存入字典。由于作者用的是LRU淘汰算法点击打开链接,可以概括为如果数据最近被访问过,那么将来被访问的几率也更高。当临界内存到的时候,就把链表尾部节点优先淘汰,这就解决了如何处理内存超过预期值的时候如何清理内存的策略。想一下,如果用数组,那么你要把用到的值拿出来,再插入到头部,显然没有双向链表高效,直接移动就好了。那么平时内存正常的情况下存取都是在NodeMap的Dict里面操作的,只有超负荷了,才会有链表淘汰策略。对应的SD用的是NSCache,系统自带的策略,具体我也没研究过,知道的可以留下言,取的时候就简单了,直接根据key,去链表的dict拿就行了,拿到的Node里面value字段就是值
握草有完没完,那么多知识点。。。。。。
下面看看磁盘缓存
- (void)setObject:(id)object forKey:(NSString *)key {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
// 扩展 数据 可先不看
if (!value) return;
NSString *filename = nil;
// 敲黑板 划重点 _inlineThreshold 默认 20k
// iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
/*
YYKVStorage 不是数据库存储 而且大于20k 文件名不为空 优先写入文件
存在filetype 和 mixType 前者很容易理解,直接写入 一般都是mixtype 后续判断是有文件名写文件,否则写入数据库 因此这里文件名的判断是 >20 写文件, 小于的话就写数据库 原因上面YY大神已经测评过了,重点 性能优化得益于此
*/
if (_kv.type != YYKVStorageTypeSQLite) {
// 数据大于 20k
if (value.length > _inlineThreshold) {
// 文件名 MD5
filename = [self _filenameForKey:key];
}
}
// 加锁访问数据库写入 写入的都是value元数据 未解码
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
// YYKVStorageTypeFile 写文件 文件名不能为空
// YYKVStorageTypeSQLite 忽略文件名
// YYKVStorageTypeMixed 当文件名不为空就写入文件,否则写数据库
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
if (key.length == 0 || value.length == 0) return NO;
if (_type == YYKVStorageTypeFile && filename.length == 0) {
return NO;
}
if (filename.length) {
// 写文件
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
// 写数据库
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
// 写数据库
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
int timestamp = (int)time(NULL);
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); // URL MD5
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); // 写入文件名
sqlite3_bind_int(stmt, 3, (int)value.length); // 元数据size
// 文件名不存在 会写入数据库 inline_data ---> data.bytes 我打印出来是内存地址 data是二进制
if (fileName.length == 0) {
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
// 编辑时间
sqlite3_bind_int(stmt, 5, timestamp);
// 最后写入时间
sqlite3_bind_int(stmt, 6, timestamp);
// 扩展元数据
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
磁盘缓存用到的是sqllite3和写文件的方式混合使用,上面的代码依旧不想看可以不看,直接看我的分析就可以了
首先明白,filename什么时候会有值?当value.length大于20k的时候,filename就有值
然后一般都是混合枚举类型,当有filenam值的时候,写入文件,写入失败,写入数据库
写入数据库部分里面的inline_data存储的就是data.bytes,这个值我打印出来是地址,那这个20k临界值性能最好?下面是作者测评说的
为此我评测了一下 SQLite 在真机上的表现。iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
终于搞清楚了,这句代码总很简单吧。。。。。。太年轻了兄弟,如果是GIF,这句代码里面很复杂
这才是核心代码啊,内部会调用刷新定时器缓存等操作,具体概括起来
在重置之后,所有的参数定时器重新开启,进行GIF的图片播放,详细情况这里不说了,可以查看传送门
可以看到,按作者所说,和之前的其他框架有一些简单的区别和性能上的优化,虽然还在用NSURLConnection。
1.通过pthread_metux和dispatch_semaphore性能极好的锁来保证线程安全
2.内存缓存层面通过双向链表和NSDictionary实现LRU淘汰算法,清理缓存策略
3.磁盘缓存通过写文件和sqllite3来进行不同大小数据的选择优化
4.针对SD而言,更好的实现GIF图片的播放
内存NSCache和磁盘FileManager | 内存双向链表 + dict + LRU 和 磁盘 FileManager 和Sqlite3 |
NSURLCOnnection | NSURLSession |
不支持GIF | 支持GIF |
@synchronize锁 | pthread和dispatch_semaphore |
下载任务全局管理,barrier队列一个个执行 | 挂载的方式一个UI对应一个任务 |
对了,这里有个小知识点
SDWebImage对GIF播放是支持的不好的,可以看解码GIF的时
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
UIImage *staticImage;
if (count <= 1) {
staticImage = [[UIImage alloc] initWithData:data];
} else {
// we will only retrieve the 1st frame. the full GIF support is available via the FLAnimatedImageView category.
// this here is only code to allow drawing animated images as static ones
#if SD_WATCH
CGFloat scale = 1;
scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
CGFloat scale = 1;
scale = [UIScreen mainScreen].scale;
#endif
CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MAC
staticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endif
CGImageRelease(CGImage);
}
CFRelease(source);
return staticImage;
}
可以看到这里SD作者都有些注释,说只支持显示第一帧的图片,如果要很好的GIF支持,请用FLAnimatedImage
这个框架能很好的显示GIF,可以简单看下实现思路,和YYImage实现的基本一致,都是算好每一帧的时间,根据时间通过CADisplayLink来播放,应该没理解错的话,如果有问题,请留言指正。
那么有三个解决方法
1.GIF直接用YYWebImage
2.用SDWebImage和FLAAnimationImage混合 这里有介绍点击打开链接,无非就是在SD的API下面,有一个setImageBlock,如果有实现,SD就不不会帮我们赋值,需要我们自己实现赋值,我们让SD下载图片,然后用FLA异步解码显示,记住要异步解码啊
3.点击打开链接这个哥们有个替代UIImage+GIF的M文件替换SDWebImage框架里面的,也行
不过三个方法都摆在这里了,用哪个看个人喜好喽
早些时间就看过,只是没那么认真分析,这次全部记下来了,吃透,妥妥的
SDWebImage源码分析
YYImage源码分析
YYModel源码分析
YYText源码分析